2023年3月

一、背景

最近业务要求PC端系统登录使用APP应用扫码登录。

主要目的是:

1、简化用户录入账号密码,达到快速登录PC;

2、账号登录使用更加安全性;

3、为了推广更多让大家打开使用APP(因为行业的特殊性,实际业务场景中大都设计师都在使用PC端设计软件,同时也习惯了PC端下单)。

二、处理流程

1、业务流程图

因为扫码的时候有两种处理逻辑,所以流程图有业务处理方案。但不管哪种方案,背后技术处理逻辑是一样的。

2、技术实现设计流图程

3、处理步骤说明

a、用户打开PC登录页面,PC登录页面向认证中心发起请求,认证中心生成uuid等信息,返回uuid等信息给前端,前端展示一个包含uuid的二维码。

b、PC端登录页面定时向认证中心轮询二维码的状态。

c、用户登录移动端,打开移动端摄像头扫描PC端登录页面的二维码。

d、移动端将二维码中包含的uuid等信息发送给认证中心,认证中心将二维码状态设置为“扫描成功”。

e、PC端登录页面轮询到二维码状态为“扫描成功”,提示“扫描成功”,以下图片仅供参考。

f、移动端展示消息确认弹出框,显示“登录”、“取消登录”按钮,同时将移动端当前登录的用账号、当前移动端登录的token和二维码uuid等信息发送给认证中心。

g、认证中心将用户所选要登录的账号保存在二维码信息里面,并将二维码状态设置为“已授权”。

h、登录页面从轮询二维码不存在时,提示“二维码已过期” ,以下图片仅供参考。

i、登录页面从轮询二维码状态为“已取销”时,提示“你已取消此次操作,你可再次扫描,或关闭窗口”。

j、登录页面从轮询二维码状态为“已授权”时,认证中心生成PC端登录的token,设置cookie,并向PC端前端发起重定向跳转。

程序处理时序图

三、代码实现

auth2认证最简单的代码结构示例

public class Auth2Login {
    public static void main(String[] args) {
        //Step 1: 获取授权请求URL
        String authRequestUrl = "https://example.com/oauth/authorize";

        //Step 2: 向授权服务器发送请求,获取授权码
        String authCode = getAuthCode(authRequestUrl);

        //Step 3: 使用授权码,向认证服务器发送请求,获取access token
        String accessToken = getAccessToken(authCode);

        //Step 4: 使用access token,访问资源服务器,进行用户登录
        String userInfo = getUserInfo(accessToken);

        //Step 5: 根据user info进行用户登录
        login(userInfo);
    }

    public static String getAuthCode(String authRequestUrl) {
        //TODO
        return null;
    }

    public static String getAccessToken(String authCode) {
        //TODO
        return null;
    }

    public static String getUserInfo(String accessToken) {
        //TODO
        return null;
    }

    public static void login(String userInfo) {
        //TODO
    }
}

扫码登录认证关键代码片段

/**
     * 初始化,主要通过请求基本参娄生成UUID,并把uuid写入redis
     * @param cmd 请求参数
     * @return
     */
    public Response init(LoginQrCodeInitCmd cmd) {

        String clientId = cmd.getClientId();
        String clientRedirectUri = cmd.getClientRedirectUri();

        ClientDetailsE clientDetails = oauthService.loadClientDetails(clientId);
        if (clientDetails == null || clientDetails.getId() == null) {
            return Response.buildFailure(AuthcenterCode.INVALID_CLIENT, String.format(AuthcenterCode.INVALID_CLIENT.getDesc(), clientId));
        }

        if (!clientDetails.getGrantTypes().contains(GrantType.QR_CODE.toString())) {
            return Response.buildFailure(AuthcenterCode.INVALID_GRANT_TYPE, String.format(AuthcenterCode.INVALID_GRANT_TYPE.getDesc(), clientId));
        }

        LoginQrCodeE qrCodeE = LoginQrCodeE.instance().init(clientId, clientRedirectUri);
        return DataResponse.of(BeanToolkit.instance().copy(qrCodeE, LoginQrCodeCO.class));
    }

    /**
     * 通过UUID获取登录二维码
     * @param uuid 唯一字符串
     * @return QR code对象
     */
    public LoginQrCodeE getLoginQrCode(String uuid) {
        return LoginQrCodeE.instance().of(uuid);
    }

    /**
     * 通过UUID扫码
     * @param uuid 唯一字符串
     * @return
     */
    public Response scan(String uuid) {
        LoginQrCodeE.instance().scan(uuid);
        return Response.buildSuccess();
    }

    /**
     * 取消登录确认
     * @param uuid 唯一字符串
     * @return
     */
    public Response cancel(String uuid) {
        LoginQrCodeE.instance().cancel(uuid);
        return Response.buildSuccess();
    }

    /***
     * 验证登录
     * @param cmd 用户登录对象信息
     * @return 如果成功返回登录信息结构体
     */
    public Response authorize(LoginQrCodeAuthorizeCmd cmd) {

        String uuid = cmd.getUuid();
        String selectedAccountId = cmd.getSelectedAccountId();
        String token = cmd.getToken();

        //是否有扫码
        if (LoginQrCodeE.instance().of(uuid).notScanned()) {
            return Response.buildFailure(AuthcenterCode.QR_CODE_NOT_SCANNED);
        }

        /**
         * 找出token
         */
        AccessTokenE accessTokenE = oauthRepository.findAccessToken(token);
        if (accessTokenE == null) {
            return Response.buildFailure(AuthcenterCode.INVALID_TOKEN);
        }

        AccountE userAccount = oauthRepository.findAccountByToken(token);
        if (userAccount == null) {
            // 当前令牌不存在用户态(账号)
            return Response.buildFailure(AuthcenterCode.TOKEN_ACCOUNT_RELA_NOT_EXIST);
        }
        List<String> userAccountIds = accountRepository.forceGetAccountIdsByMainUserId(userAccount.getMainUserId());
        if (userAccountIds == null) {
            // 当前账号异常
            return Response.buildFailure(AuthcenterCode.UNKNOWN_ACCOUNT);
        }

        if (!userAccountIds.contains(selectedAccountId)) {
            // 所选账号与当前令牌登录人信息不一致
            return Response.buildFailure(AuthcenterCode.INVALID_SWITCH_ACCOUNT);
        }

        LoginQrCodeE.instance().authorize(uuid, selectedAccountId);
        return Response.buildSuccess();
    }

    /**
     * 对外提供轮旬时间服务方法,当查询redis key=uuid是否超时
     * @param uuid 用户访问请求的UUID
     * @return 登录码状态对象
     * @throws OAuthSystemException
     */
    public LoginQrCodeE handle(String uuid) throws OAuthSystemException {
        LoginQrCodeE loginQrCode = getLoginQrCode(uuid);
        // 当处于“已授权”状态时,才能触发准备登录
        if (loginQrCode.authorized()) {
            return loginQrCode.ready();
        }

        // 当处于“准备登录”状态时,才能触发登录
        if (loginQrCode.loginReady()) {
            return login(loginQrCode);
        }

        return loginQrCode;
    }

    /**
     * 扫码登录
     * @param loginQrCode 二维码带的对象信息
     * @return
     * @throws OAuthSystemException 认证异常
     */
    public LoginQrCodeE login(LoginQrCodeE loginQrCode) throws OAuthSystemException {
        String clientId = loginQrCode.getClientId();
        String accountId = loginQrCode.getAccountId();
        ClientDetailsE clientDetails = clientDetailsRepository.findByClientId(clientId);
        AccountE userAccount = accountRepository.getAccountById(accountId);
        accountRepository.checkAccount(userAccount);
        AuthorizeE authorize = oauthRepository.findAccountAuthorizeByAccountId(accountId);
        authorizeRepository.checkAuthorizeDataIntegrity(authorize);
        if (authorize == null) {
            throw new UnknownAuthorizeException("Cannot find AuthorizeE mainUserId="+mainUserId);
        }
        AccessTokenE accessToken = oauthService.retrieveQrCodeAccessToken(clientDetails, authorize, userAccount, new HashSet<>(),
                new BizCodeE(loginQrCode.getAppCode(), loginQrCode.getSubAppCode()));

        return loginQrCode.login(accessToken.getToken(), accessToken.getRefreshToken(), accessToken.getCastgt());
    }

代码仅是展示关键的处理过程,结构还是比较清晰的;这里不提供完整的项目工程,因为这是公司的产权,况且每个公司的业务要求不同,大家理解后再去实现的自己扫码认证逻辑,处理方法大同小异。

背景

日常开发中,经常需要对一些响应不是很快的关键业务接口增加防重功能,即短时间内收到的多个相同的请求,只处理一个,其余不处理,避免产生脏数据。这和幂等性(idempotency)稍微有点区别,幂等性要求的是对重复请求有相同的
效果

结果
,通常需要在接口内部执行业务操作前检查状态;而防重可以认为是一个业务无关的通用功能,在ASP.NET Core中我们可以借助过Filter和redis实现。

关于Filter

Filter的由来可以追溯到ASP.NET MVC中的ActionFilter和ASP.NET Web API中的ActionFilterAttribute。ASP.NET Core将这些不同类型的Filter统一为一种类型,称为Filter,以简化API和提高灵活性。ASP.NET Core中Filter可以用于实现各种功能,例如身份验证、日志记录、异常处理、性能监控等。

image

通过使用Filter,我们可以在请求处理管道的特定阶段之前或者之后运行自定义代码,达到AOP的效果。

image

编码实现

防重组件的思路很简单,将第一次请求的某些参数作为标识符存入redis中,并设置过期时间,下次请求过来,先检查redis相同的请求是否已被处理;
作为一个通用组件,我们需要能让使用者自定义作为标识符的字段以及过期时间,下面开始实现。

PreventDuplicateRequestsActionFilter

public class PreventDuplicateRequestsActionFilter : IAsyncActionFilter
{
    public string[] FactorNames { get; set; }
    public TimeSpan? AbsoluteExpirationRelativeToNow { get; set; }

    private readonly IDistributedCache _cache;
    private readonly ILogger<PreventDuplicateRequestsActionFilter> _logger;

    public PreventDuplicateRequestsActionFilter(IDistributedCache cache, ILogger<PreventDuplicateRequestsActionFilter> logger)
    {
        _cache = cache;
        _logger = logger;
    }

    public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    {
        var factorValues = new string?[FactorNames.Length];

        var isFromBody =
            context.ActionDescriptor.Parameters.Any(r => r.BindingInfo?.BindingSource == BindingSource.Body);
        if (isFromBody)
        {
            var parameterValue = context.ActionArguments.FirstOrDefault().Value;
            factorValues = FactorNames.Select(name =>
                parameterValue?.GetType().GetProperty(name)?.GetValue(parameterValue)?.ToString()).ToArray();
        }
        else
        {
            for (var index = 0; index < FactorNames.Length; index++)
            {
                if (context.ActionArguments.TryGetValue(FactorNames[index], out var factorValue))
                {
                    factorValues[index] = factorValue?.ToString();
                }
            }
        }

        if (factorValues.All(string.IsNullOrEmpty))
        {
            _logger.LogWarning("Please config FactorNames.");

            await next();
            return;
        }

        var idempotentKey = $"{context.HttpContext.Request.Path.Value}:{string.Join("-", factorValues)}";
        var idempotentValue = await  _cache.GetStringAsync(idempotentKey);
        if (idempotentValue != null)
        {
            _logger.LogWarning("Received duplicate request({},{}), short-circuiting...", idempotentKey, idempotentValue);
            context.Result = new AcceptedResult();
        }
        else
        {
            await _cache.SetStringAsync(idempotentKey, DateTimeOffset.UtcNow.ToString(),
                new DistributedCacheEntryOptions {AbsoluteExpirationRelativeToNow = AbsoluteExpirationRelativeToNow});
            await next();
        }
    }
}

PreventDuplicateRequestsActionFilter里,我们首先通过反射从
ActionArguments
拿到指定参数字段的值,由于从request body取值略有不同,我们需要分开处理;接下来开始拼接key并检查redis,如果key已经存在,我们需要短路请求,这里直接返回的是
Accepted (202)
而不是
Conflict (409)
或者其它错误状态,是为了避免上游已经调用失败而继续重试。

PreventDuplicateRequestsAttribute

防重组件的全部逻辑在
PreventDuplicateRequestsActionFilter
中已经实现,由于它需要注入
IDistributedCache

ILogger
对象,我们使用
IFilterFactory
实现一个自定义属性,方便使用。

[AttributeUsage(AttributeTargets.Method)]
public class PreventDuplicateRequestsAttribute : Attribute, IFilterFactory
{
    private readonly string[] _factorNames;
    private readonly int _expiredMinutes;

    public PreventDuplicateRequestsAttribute(int expiredMinutes, params string[] factorNames)
    {
        _expiredMinutes = expiredMinutes;
        _factorNames = factorNames;
    }

    public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
    {
        var filter = serviceProvider.GetService<PreventDuplicateRequestsActionFilter>();
        filter.FactorNames = _factorNames;
        filter.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(_expiredMinutes);
        return filter;
    }
    public bool IsReusable => false;
}

注册

为了简单,操作redis,直接使用微软
Microsoft.Extensions.Caching.StackExchangeRedis
包;注册
PreventDuplicateRequestsActionFilter

PreventDuplicateRequestsAttribute
无需注册。

builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = "127.0.0.1:6379,DefaultDatabase=1";
});
builder.Services.AddScoped<PreventDuplicateRequestsActionFilter>();

使用

假设我们有一个接口
CancelOrder
,我们指定入参中的OrderId和Reason为因子。

namespace PreventDuplicateRequestDemo.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class OrderController : ControllerBase
    {
        [HttpPost(nameof(CancelOrder))]
        [PreventDuplicateRequests(5, "OrderId", "Reason")]
        public async Task<IActionResult> CancelOrder([FromBody] CancelOrderRequest request)
        {
            await Task.Delay(1000);
            return new OkResult();
        }
    }

    public class CancelOrderRequest
    {
        public Guid OrderId { get; set; }
        public string Reason { get; set; }
    }
}

启动程序,多次调用api,除第一次调用成功,其余请求皆被短路
image

查看redis,已有记录
image

参考链接

https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/filters?view=aspnetcore-7.0
https://learn.microsoft.com/en-us/aspnet/core/performance/caching/distributed?view=aspnetcore-7.0

更多技术交流、求职机会,欢迎关注
字节跳动数据平台微信公众号,回复【1】进入官方交流群

导读:经过十多年的发展,数据治理在传统行业以及新兴互联网公司都已经产生落地实践。字节跳动也在探索一种分布式的数据治理方式。本篇内容来源于火山引擎超话数据直播活动的回顾,将从以下四个部分展开分享:

  • 字节的挑战与实践

  • 数据治理的发展与分布式

  • 分布式自治架构

  • 分布式自治核心能力

字节的挑战与实践

首先来看一个问题:“一家公司,数据体系要怎么搭建?”

  • 方案一:
    整体规划,系统架构驱动

  • 方案二:
    问题出发,业务价值驱动

在字节跳动,我们选择的是方案二,即从业务遇到的问题出发,重视落地结果与业务过程,去解决实际的治理问题。

基于这个理念,在数据治理过程中,字节跳动也面临以下三个挑战与机遇:

业务特点:业务发展快、场景丰富、数据量大且形态各异。业务的线上服务及创新,都对数据有较强的依赖,核心业务数据延迟,质量问题将直接影响业务表现及发展。

组织特点:扁平化的组织模式,分布式的组织管理。无行政手段或强组织约束,也无全局治理委员会,且数据从采集到应用全部的生产流程,没有全局规范,业务团队需要自主制定策略并落地。

文化特点:OKR 拆解与对齐文化
,业务团队有充足的目标定义与拆解权限,且任何人都可能有动机、有角色、甚至有权限去进行数据治理,导致数据治理的业务流程复杂

字节数据治理演进阶段

字节数据治理演进阶段分为 6 个阶段:

  1. 业务第一原则:坚持业务第一原则,解决业务实际遇到的治理痛点

  2. 优先稳定建设:优先解决交付稳定,保障数据链路与产出稳定,减少交付延迟

  3. 保障数据质量:核心链路质量管控,配置强质量规则,自动熔断,避免全链路数据污染;加强事前检查,从源头加强质量控制;完善事后评估,为每一张表建立健康档案,持续改进。

  4. 关注数据安全:冗余权限识别,消除授权风险;数据分类分级,风险定义与多策略控制,减少安全风险

  5. 重视成本优化:基于多种规则的与完备的治理元数仓,提供低门槛的治理产品能力,快速优化存储

  6. 提高员工幸福感:在帮助业务完成数据治理的后,还需要考虑团队的负载压力,报警治理,降低员工起夜率;归因分析,快速排查修复故障。

在这里,再介绍字节特色的“0987”量化数据服务标准。这四个数字分别指的是:稳定性 SLA 核心指标要达到 0 个事故,需求满足率要达到 90%,数仓构建覆盖 80% 的分析需求,同时用户满意度达到 70%。按照这个高标准来要求自己,同时这也是一种自监管的机制,能够有效的防止自嗨,脱离业务需求和价值。

字节的部分场景实践

下面通过两个例子为大家介绍数据治理在字节的场景实践。

案例一:

  • 问题:字节跳动内部 2019 年到 2020 年间,双月内事故数量较多,对业务造成一定影响,且收敛困难,每天都有告警、起夜、对正常开发进度造成影响。

  • 解决方案: 采用了分布式用户自治的 SLA 治理,通过数据分级保障目标管理,在各业务内部进行【拉齐链路-数据分级-广泛共识-系统管理】的行动闭环,系统化保障目标传递和落地。

  • 效果: 截止 2020 年中,事故以每双月 30%环比下降,在 1 年内达到稳定性问题彻底收敛。

案例二:

  • 问题:抖音的实时数仓治理人员的精力分散,以被动的运动式、“救火”式的工作模式为主。协同效率低,人力投入巨大,缺少可持续性。

  • 解决方案: 覆盖质量、成本、SLA、安全等治理方向,以业务评估体系,构建治理方案进行例行诊断,对存量问题进行识别和派发,形成一套【评估->识别->规划->执行->复盘】业务内部分布式自治的治理机制。

  • 效果: 从 21 年至今,治理人员的精力彻底从”运动式“治理的模式中解放出来,更多精力会集中在监督执行与规则优化中,团队起夜率降低 30%。质量保障覆盖率达到 100%。双月存储优化均在 20+PB。

数据治理的发展与分布式

众所周知,有很多机构都分享了对数据治理的定义,这里简单分享一下

国际数据管理协会(DAMA): 数据治理是对数据资产管理行使权力和控制的活动集合

IBM:数据治理是对企业中的数据可用性、相关性、 完整性和安全性的全面管理。它帮助组织管理 他们的信息知识和作为决策依据

维基百科对数据治理的定义:数据治理是一个涉及全体组织的数据管理概念,通过数据治理,确保在数据的整个生命周期中拥有高数据质量的能力,也是对业务目标的支持。数据治理的关键的重点领域包括可用性、一致性、数据完整性和数据安全性,也包括建立流程来确保整个企业实施有效数据管理。

在传统的数据治理方法论与定义中,注意到他有以下共性特点,同时也是现在大多数公司的实践路径,即:

但是在实际的执行过程中,他需要以下几个前提和随之带来的落地难点

1.需要明确组织制度

梳理业务数据部门,设立公司级别数据治理委员会/部门,各业务分设执行部门,公司内各业务宣导讨论,统一制定公司数据治理规章制度

难点一:组织依赖重、建设周期长。需要招聘大量专业的治理专家或引入外部咨询机构,计划制定周期长;专设部门牵头,若无自顶向下的项目背景,业务协调对齐困难。

2. 需要明确权责管理

梳理公司数据资产,迁移、拆分、业务改造。确保资产归属与治理权责明确,定期梳理资产类目,维护资产元数据的有效性,确保治理边界清晰

难点二:业务影响大,目标对齐难。需完成存量的资产归属划分、改造生产开发体系,对增量定期人力打标,确保资产归属与权责边界清晰,因可能业务系统改造,会对业务发展造成影响

3.需要进行复盘抽查

管理组织定期检查各业务治理过程是否符合公司治理制度,定期检查各项治理结果是否落地,线下复盘与推动不符合预期的治理过程

难点三:沟通成本高,执行推动难。如何制定适用于不同业务特点与发展阶段的团队的治理评估体系,各团队是否认可评估标准。

为了解决以上三个问题,我们有些新的思考,即引入「分布式」的理念。

Governance 一词在根源上同 Government,1990 年代被经济学家和政治科学家重新创造,由联合国、世界货币组织和世界银行等机构进行传播。其核心有以下两种论述:

第一个论述:标准与规范。指的是一定范围内的一致的管理,统一的政策,某一责任区指导以及合适的监管和可问责机制。这种行政力的集中化管理存在一些问题,比如决策成本高,人力投入高、落地阻力大,精力消耗大。

第二个论述:过程与结果。指的是只要关注结果和产出以及业务内部实践,通过分布式协作让业务的治理结果、业务痛点和治理方式及手段在内部闭环,而不是由中台层面统一推动。

我们尝试从第二种论述,即重视过程落地和治理结果产出的出发,更快的落地产品,落地数据治理的产品解决方案

从集中式到分布式

基于分布式的数据自治的理念,我们来解决在落地执行上的两个最困难的点

一、组织制度分布式:尝试将组织的强管理属性转换到监督属性,治理单元与制度设计回归到业务单元。好处是,不强依赖横向中心化组织,业务治理痛点闭环在业务单元,且业务基于自身发展阶段制定治理目标,ROI 论证回归业务。

二、权责验收分布式:基于产品体系与落地解决方案,支持业务按需自驱,市场化执行,平台辅助与按需验收。好处是,无须长周期的资产类目梳理,业务系统改造,权责均由业务区分,基于业务单元与多维视角,按需验收治理结果,业务单元内对齐。

如上图展示的饼图,对于一个公司的数据资产,传统来说,可以很清晰地按照业务边界来划分清楚。对于分布式数据治理,我们通常是由业务单元自行认领,业务单元 A 自行认领属于自己部分,业务单 B 也自行认领属于自己部分。认领就意味着,所有治理的动作包括结果,安全性、成本、质量、稳定都由认领业务单元负责。

当然,这样这样也可能存在两个问题,不过在分布式的理念中能够得到较好解决

第一是认领范围重合:这种情况往往让业务在线下对齐是否需要去做改造和划分,各自拿到自身需要的治理结果,短期无须重人力投入,不追求绝对的边界划分。长期因不同治理验收需求或团队管理需求,自行进行资产归集和整理。达到动态的平衡状态

第二是无人认领:针对长期无人认领的资产,我们可以基于每个业务的历史的规则和能力,形成一个治理的平均线,再从平台层面推动无人认领的资产治理,由于无人认领,这样的资产推动起来相对较快。

我们理解的分布式治理

定义:以业务单元为数据治理闭环单元,通过完善的产品工具,将管理视角转化为监督视角,解决数据治理落地痛点;各业务团队分布式自运行,整体上达到全局最优,从形态上,适配更多业务特性和发展阶段,从效果上,强推进重落实与结果

字节跳动通常以业务单元作为一个数据治理闭环,即在业务单元内部完成数据稳定性、质量、存储、计算等治理。同时每个业务单元不是孤立的,也有相互协作,比如 A 业务单元的数据治理经验可以沉淀为治理模板,供后续其他业务使用。

这样的分布式治理方式,有以下一些优势:

  • 影响小,依赖小。治理下放到各个业务中,各级业务乃至个人都能自驱治理,业务根据自身发展阶段灵活组合治理工具,无须对组织强依赖。

  • 周期短,见效快。业务自驱梳理核心数据及链路,跨团队对齐线上化、协议签署、过程追踪。治理周期显著缩短,很快就出成效,增强团队信心。

  • 效率高,省人力。SLA 治理提高跨团队协作效率,聚焦核心数据任务集中资源保障,集中精力,报警归因减少起夜,帮助企业节省年度人力消耗

  • 算清帐,降成本。各业务口径的存储计算资源消耗、核算成本,制定降本目标并追踪落地;业务经验规则化、策略化、自动化、自驱化持续降本增效。

分布式自治架构

为达成业务分布式自治,产品需要对用户行为路径完全覆盖,对业务经验完全接受。平台提供完善的开放能力,协助业务进一步提效

产品体系

以上关于分布式的理解,下面将介绍字节分布式自治的产品体系。

从治理门户来看,包括治理全景、工作台、规划、诊断、复盘等全流程治理环节。在治理场景中,提供数据质量安全、资源优化、报警、企业复盘管理等一系列垂直场景。在底层,包含数据全生命周期流程,从数据采集、数据传输、数据存储、数据处理、数据共享到数据销毁。

治理双路径

为了把用户所有治理经验沉淀为平台能力,我们抽象了 2 种治理路径。

  • 第一种是规划式路径。这是一个比较常见的规划式路径,即从看板和报表出发,自上而下做规划。比如看板已经反映出成本增加、延时变长或者数据质量变差,团队管理者发起报告或事故,推动业务单元同事进行数据治理,最后进行复盘。

  • 第二种是响应式。比如生产者收到一个数据质量或延时的报警,随后快速定位原因并做改进计划。

为了更好把业务经验全部线上化,我们通常双路径并行使用。

规划式治理路径案例

首先看通用模块资产视图,包括资产增量情况评估等,以及业务对于资产的评价,如健康分体系。我们通常根据资产情况去制定目标。如果发现问题之后,业务驱动制定目标,可能是降低存储。同时需要去应用一些业务规则,比如团队内部认为 TTL(数据生命周期)很重要,需要帮助识别出来的同时也需要设定一个诊断周期。在团队方案确认完之后,产品会做监督,包括定义提醒,同时也推动资产 owner 完成总结。

响应式治理路径案例

例如,我们发现一些任务在深夜执行失败了,需要先做问题排查,发现问题是 HDFS 丢块导致。在传统情况下,解决方案是去检查 API 问题,再去拉相关人员,可能 2- 3 小时才能完成,最后配合监控并收归到 wiki 中。而在 DataLeap 数据治理产品里,可以直接实现归因打标等能力,最后快速复盘。

治理全规则

如果要覆盖业务的全部属性,治理平台需要形成有效且全面的规则模板。目前,我们的规则模板包含两个部分:

第一是规则引擎,具体包括业务输入、平台输入、推荐输入。

  • 业务输入:主要依据业务团队的治理经验以及行业经验。

  • 平台输入:平台会提供一些基础能力,如存储、计算、质量、报警等几个维度。截止目前已经提供了 80 多个规则。

  • 推荐输入:基于业务输入和平台输入,去做分析和挖掘,发现哪些规则用得多、哪些规则阈值更合理。

第二是治理数仓,具体包括行为数据、治理操作、效果数据。

  • 行为数据:包括用户规则配置等内容是否有重复以及带元素标签的资产数据等。

  • 治理操作:包括生命周期、任务关闭、数据删除、SLA 签署等。

  • 效果数据:包括操作收益、资产收益、指标收益等。

不同业务快速灵活接入治理规则

分布式自治基础是要构建治理生态、建设开放平台,让不同业务能够快速、灵活接入。

为了让业务能快速介入,我们把数据分成了四种类型:表达式、三方元数据、标准元数据、算法包。针对不同的业务,根据当前的经验和能力,我们会提供不同的接入方式,让业务去更好把规则和能力去接入到我们的平台。

基于业务单元进行智能化提效

在获取不同业务的规则和能力之后,我们需要再做平台能力沉淀,把好的规则和能力复用给更多业务。

Case1:任务 SLA 签署推荐。基于运营时间做权重分配,保证下游任务运行完成,同时也会进行关键链路分析。这个规则目前在字节内部广泛使用。

Case2:动态阈值监控。这是基于业务在报警阈值上的实践提取的规则。

Case3:相似任务识别。通过序列化和向量化操作,去和底层 spark 引擎做配合。在业务内部应用覆盖 99%,且优化任务都千级以上,由此接入平台并推荐给其他业务。

分布式治理核心能力

治理全景-分布式验收

在分布式验收中,会区分为全员视角、团队视角和个人视角。全员视角可以看到公司级资产,包括整体的健康分体系以及核心指标。团队视角中,主要由业务自己梳理,包括内部的评价体系。

治理工作台-集中治理待办

上图为个人工作台功能,主要为了把 SLA 保障、计算任务、数据存储等治理场景展示在一个页面,方便 owner 业务全局查看治理待办事项。

治理规划与诊断-权责与规划分布式

第一,支持自定义治理域,灵活自治,提供多种维度,自定义组合和圈选资产范围。

第二,支持创建治理方案,例行诊断:发起人基于业务需求,选择治理域,设计治理规则,发起存储/计算/质量等类型治理方案。例行诊断与推进实施。

第三,支持规则管理,提供 80+治理基础规则,支持自定义组合和配置规则与分享。

复盘管理

复盘管理是一个通用模块。业务根据自身需要去识别任务是否需要复盘,或者仅仅做问题登记。除此之外,业务还可以用复盘管理能力做内部管理,比如查看、检索所有的事故复盘,查看每个事故发生的原因和改进计划。同时,也可了解归因分布情况,并帮助下一个值班同学快速反馈和定位问题。

SLA 治理

在字节跳动内部,SLA 不是平台级保障,而是源于业务团队内部。首先是业务按需申报,可能是 PM、运营或数据研发等任何角色,认为自身任务重要,填写背景、原因、等级、时间等信息之后,即可发起一个 SLA。发起之后,在团队内部进行审核,可能存在同一个团队多个高优任务的情况,这由团队内部自行调整优先级。同时,这个也是跨团队判断该任务重要性的标准。

之后是完成签署,签署也会在产品里面体现出来。每个节点时间都有实时监控,如果产生了延迟,会推动业务做复盘和登记。我们也提供基础的 DAG,包括申报业务单的查看,同时也可以让大家去查看每个等级的破线情况,以及团队对业务的服务情况。

数据安全

在数据安全层面,主要专注于清理冗余权限,完善分类分级。不同团队对冗余权限定义不同,有的 90 天无访问算冗余权限,有的 70 天,有的 7 天。因此我们提供自定义能力,由业务内部发起 review,完成冗余权限的识别和定义规则,识别之后复用诊断能力。

资源优化

基于每个团队实际执行情况,提炼出一些通用的规则。例如,某些规则可能有几十个业务在使用,近 90% 认为近 30 天无查询需要被识别出来,我们就会在平台中提供这类能力,方便新业务或者小白业务去使用。

报警归因

在报警归因方面,我们能提供所有报警明细,方便查看是否有重复规则,是否有高频报警规则,帮助用户发现无效报警和重复规则,降低告警量和跟起夜率。除此之外,我们也提供业务内部的归因登记和分析能力。

以上是字节跳动在数据治理相关实践。

目前,字节跳动也将沉淀的数据治理经验,通过火山引擎大数据研发治理套件 DataLeap 对外提供服务。作为一站式数据中台套件,DataLeap 汇集了字节内部多年积累的数据集成、开发、运维、治理、资产、安全等全套数据中台建设的经验,助力 ToB 市场客户提升数据研发治理效率、降低管理成本。

点击跳转
大数据研发治理套件 DataLeap
了解更多

本篇参考:

salesforce零基础学习(一百一十一)custom metadata type数据获取方式更新

https://developer.salesforce.com/docs/atlas.en-us.apexref.meta/apexref/apex_methods_system_custom_metadata_types.htm

https://help.salesforce.com/s/articleView?id=sf.custommetadatatypes_overview.htm&language=en_US&type=5

https://trailhead.salesforce.com/content/learn/modules/custom_metadata_types_dec/cmt_overview

https://developer.salesforce.com/docs/atlas.en-us.224.0.apexcode.meta/apexcode/apex_class_Metadata_Operations.htm

我们在之前的篇中简单描述了 custom metadata type的使用,最开始的 custom metadata type是来建议取代 list custom setting,好处是可以基于metadata进行部署,不用像custom setting基于数据方式,容易出现漏部署情况,所以基于当时的版本来说, custom metadata type相对 list custom setting来说好处是基于metadata部署避免遗漏。

随着 custom metadata type的不断升级,目前增加了很多吸引人的点,主要是针对 Metadata Relationship字段类型的展开,接下来我们看一下这个新的类型可以实现的内容和场景。

一. Metadata Relationship类型

当我们在 custom metadata type创建字段时,目前字段类型增加了Metadata Relationship类型,此种类型可以设置两类的关联关系。

  • 关联到其他的custom metadata type,比如metadata type中维护省和市的信息,可以在市的metadata type中关联到省的metadata type。

  • 关联到salesforce标准的表或者自定义表/字段的实例中,比如关联到 Account表的 Industry字段(场景可以基于配置方式设置 default value)。

当我们点击此类型创建的下一步,会让你选择系统已经存在的 custom metadata type还是选择 Entity Definition。需要注意的是,如果你的系统曾经已经创建过 Entity Definition,那样以后的步骤中,还会再列表中可以选择
Field Definition以及
Entity Particle,这个在下面图中会有涉及。

demo中会创建这两种类型,其中关联自定义 metadata type的创建步骤不在此处罗列,主要讲一下 Entity Definition相关的关联,当我们创建了关联到 Entity Definition的字段以后,我们继续创建 Metadata Relationship类型的字段,就可以看到下图内容。其中:

  • Field Definition:关联的是上述选定的表的标准或者自定义字段
  • Entity Particle:关联的是上述选定的表的标准字段中的复合类型字段或者地理信息类型字段。

当我们选择 Field Definition类型以后,点击下一步会选择 controling field,这样就会实现当选择某个表信息以后,就可以选择到当前这个表的字段。

当我们创建表相关的表数据以后,我们就可以为custom metadata type设置数据,下图demo中维护了Account Customer Priority(自定义字段)默认值的一条数据,我们可以看到当Object Name选择了 Account以后,Field Name就可以自动的基于Account(作为 controling field)选择到 Account表中的字段。

这里针对父子的场景不做一步一步处理,感兴趣的可以基于下图进行数据的创建。

二. Custom Metadata Type使用场景介绍

1. 字段default value:
我们在项目上,有时需要在字段级别或者后台代码设置字段的默认值。原有方式是可以基于类型进行设置,比如picklist可以通过选择,其他类型就在 Default Value处设置初始值。apex端设置可以通过Custom Label或者hardcode方式写。除此以外,我们建议使用 Custom Metadata Type来统一维护初始值设置。UI方面可以基于指定的写法进行设置,格式如下图所示。

2. 用于validation rule / formula / process builder:
这里只针对validation rule进行举例,写法相同。举个例子,当系统validation rule需要配置的规则用于很多表,并且这个值可能是动态修改的,我们不能每次变更都修改所引用到的所有的validation rule,这时我们可以基于custom metadata type进行配置来更好的可配置化管理。

三. 通过apex class获取 custom metadata type

1. 获取 field Definition类型的metadata type数据

基于 apex端,我们可以通过基于metadata的方式,或者基于SOQL搜索方式获取到这条数据,然后获取这条数据的信息,下方的demo仅供参考。

1). 通过 custom metadata type的getInstance方式获取。此方法前提是你需要了解到这个metadata type的 Name信息。

Default_Value__mdt defaultValue = Default_Value__mdt.getInstance('Account_Customer_Priority_Default');
system.debug('*** default value: ' + defaultValue.Default_Value__c);

2). 通过表字段的名称获取(这里代码可以进行优化,目前demo中的场景为有且仅有一条配置)。这里我们通过 FieldDefinition获取了当前表的 DurableId,原因是custom metadata type返回的 Field Definition是 DurableId,这个表会在下一篇博客做一些介绍。

List<Default_Value__mdt> defaultValueList =Default_Value__mdt.getAll().values();

Default_Value__mdt defaultValue;

List<FieldDefinition> fieldDefinitionList =[SELECT Id, DeveloperName, DurableId
FROM FieldDefinition
WHERE DeveloperName = 'Customer_Priority'AND EntityDefinition.QualifiedApiName = 'Account'];
//TODO 实际项目中禁止此种写法,需要非空判断
String customerPriorityDurableId = fieldDefinitionList.get(0).DurableId;

for(Default_Value__mdt valueItem : defaultValueList) {
if('Account'.equalsIgnorecase(valueItem.Object_Name__c)
&&customerPriorityDurableId.equalsIgnorecase(valueItem.Field_Name__c)) {
defaultValue =valueItem;
}
}

if(defaultValue != null) {
system.debug('*** default value : ' +defaultValue.Default_Value__c);
}

2. 获取到关联其他metadata type的数据

1). 通过 custom metadata type的getInstance方式获取,方式同上,不做说明。

2) 通过关联父表的数据的developerName进行获取(这里代码可以进行优化,目前demo中的场景为有且仅有一条配置)。

List<Default_Value__mdt> defaultValueList =Default_Value__mdt.getAll().values();

Default_Value__mdt targetDefaultValue;
for(Default_Value__mdt valueItem : defaultValueList) {if(String.isNotBlank(valueItem.Parent_Metadata_Type__c)&& 'test_parent'.equalsIgnorecase(valueItem.Parent_Metadata_Type__r.DeveloperName)) {
targetDefaultValue
=valueItem;
}
}

system.debug(JSON.serialize(targetDefaultValue));

我们看一下目标数据通过 getAll返回的JSON结构,我们会发现如果有负责结构内容,会将复结构的信息同样返回。

{
"MasterLabel": "TEST_RELATION_WITH_PARENT",
"NamespacePrefix": null,
"QualifiedApiName": "TEST_RELATION_WITH_PARENT",
"Parent_Metadata_Type__c": "m005g000002FMry",
"Language": "zh_CN",
"attributes": {
"type": "Default_Value__mdt",
"url": "/services/data/v57.0/sobjects/Default_Value__mdt/m025g000000rtgz"
},
"DeveloperName": "TEST_RELATION_WITH_PARENT",
"Id": "m025g000000rtgz",
"Parent_Metadata_Type__r": {
"attributes": {
"type": "Parent_Metadata_Type__mdt"
},
"DeveloperName": "test_parent"
},
"Field_Name__c": null,
"Object_Name__c": null,
"Label": "TEST_RELATION_WITH_PARENT",
"SystemModstamp": "2023-03-15T10:00:03.000+0000",
"Default_Value__c": null
}

总结:
本篇主要是介绍了一下 metadata type除取代list custom setting以外的其他的使用场景以及使用apex获取的方式。篇中的demo也仅用于获取数据用,对判断,逻辑,可行性操作都可以进一步优化。篇中有错误地方欢迎指出,有不懂欢迎留言。

人类符号媒介系统的发展都是尝试性的。开始是为了一些具体有限的目的,人们自觉不自觉地尝试一些媒介工具与方法,方法的有效性会强化与延伸所用的工具与方法,反之则会放弃所用的工具与方法。形成系列的工具与方法,就会固化出一个媒介系统,发展出相应的语言类型。本书对语言机器的构想,把符号媒介系统的发展更多变成了技术与工程性的工作,这不会改变这里的性质。

我们并不能肯定什么变化是可接受的。一方面,语言需要最大的普及,工具与符号使用上的变化要与心智、心理、历史相适应,这些方面还是灰色地带,没有建立可以判断的标准。可以看看世界语的例子。很容易认识到我们自然语言有着各种各样的问题:发音不规范、语法不统一、到处都是规则的例外、表述总会有歧义等等。这带来了学习交流上的障碍。为了克服这些问题,一直以来人们就尝试以自然语言为母本,通过简化、规范化来构建新的人造语言。代表之一是柴门霍夫创造的世界语。经过了二、三百年的时间,人造语言的应用仍只是小众的喜好,对其最初的目标难说成功。另一方面,认知方向的符号使用最终是要建立与对象世界的一致对应,描述出对象世界。一种新符号使用方式能带来对某部分对象世界的有效刻画,别的方式没有同样的效果,此时,优势的符号使用方式甚至不需要去迁就心智、心理、历史等因素。历史的经验是:分化出的认知方向的符号使用,可以自由地快速地变化,然后牵引着自然语言的相应部分相对缓慢地进展。

变化一旦开始,能发展到什么程度也不是事前能完全确定的。进化使人类有二只手,每只手上有长短不一的五根手指,每根手指有三个关节可以让手指向内弯曲。对于进化的设计者来说,这样进化是为了让双手可以抓住树干树枝,攀爬树林。长远来看,我们获得的是一种元能力,我们可以用手从事很多的工作。进化设计者可能也没想到的是,今天我们主要用手指来敲击键盘,或点击屏幕。同样,上面所构想的语言机器能够一定程度上实现后,新的符号使用方式会影响并塑造我们的心智习性,带来不同路径的认知与思考,这些变化反过来又进一步影响着符号的使用。比如能动语言的实现,变化不会只是已有的计算以后会在新的环境以新的方式执行。有了能动语言,只要能有效果,我们会把符号规则性自动操作应用于以前未出现计算的场景。

新的语言类型成功后,在其下成长起来的后人,他们的体验与直觉不是今天的人们真正能够感受的。我们有的只是过去的经验,这些已有的经验深受现有语言使用所形成惯性的影响。凭借这些经验并不足以对未来的可能性做出准确预测与评价。最终,只有通过实践,才能发展出新的符号媒介系统,建立新的语言类型与符号使用方式。回头来看本章前几节的论述,每一部分都是从想达到的效果直接来说的,实际系统使用需要很大的灵活性,而不可能这么高的耦合度。比如结构符号的使用。写作的过程也是思想成型的过程,过程中思想是活跃而不稳定的,结构符号的选用应该是以灵活的方式支持写作过程中任何时点选用或重选,否则思想可能过早陷入僵化。整体来说,语言机器上符号媒介系统最好是能向下兼容,可以退回一种编辑工具来使用,然后可以以阶梯性的方式来增加新方式的使用,这是实用化设计的最大挑战。

通过不断实践,最终建立新的符号媒介系统与语言类型,就提供了一个新的平台,我们的思想与文明将在其上重构。与此同时,我们可以从向内分析符号媒介系统的物理层面开始,去发展初始的元语言。元语言能够模拟出机器的能力,也将确定认知与思想的边界。

本书把技术与工程加入符号媒介系统的发展进程,而技术总是不断发展,越来越快地发展,提供更多的可能性。也许下一步的技术发展后,我们会去谈论三维投影上的符号安排方式,或者量子计算带来的变化。更多的可能不是现在能预测到的。从媒介视角的语言观出发,除了原则性的观点外,并不试图以一个封闭的理论来解释符号使用。我们所能看到的符号使用方式都是当时技术条件、智力、历史等因素综合作用的结果,没有什么符号使用方式是终极模式。这也连带地使依赖于符号的认知、智力活动等成为阶段性的形式,或者说都是暂时的形态。本书的一个目标是要将语言还原回一种实践,我们将在不同的意识水平上重新回到这一最重要的实践。