许多最新的英特尔处理器都受益于称为 AVX-512 的新指令系列。这些指令在宽寄存器(最多 512 位)上运行,并遵循单指令多数据 (SIMD) 范例。这些新的 AVX-512 指令允许您打破一些速度记录,例如以内存副本的速度解码base64数据。
大多数现代处理器都有 SIMD 指令。AVX-512 指令更宽(每个寄存器更多位),但这不一定是它们的主要吸引力。如果您只是采用现有的 SIMD 算法并将它们应用于 AVX-512,您可能不会获得您想要的那么多好处。的确,更宽的寄存器是有益的,但在超标量处理器(每个周期可以发出多条指令的处理器)中,每个周期可以发出的指令数量同样重要。通常地,处理器每个周期可以发出的 512 位 AVX-512 的指令更少,因此这些指令更耗费资源。要想真正充分利用 AVX-512,开发者需要仔细设计代码,而与此同时英特尔还在不断增加新的指令。总的说,AVX-512 不是单个技术,而是一个系列指令集。
此外,AVX-512 指令的早期实现通常会导致可测量的降频:处理器会在使用这些指令后一段时间内降低其频率。值得庆幸的是,支持 AVX-512(Rocket Lake 和 Ice Lake)的最新英特尔处理器已经取消了这种系统的频率限制。值得庆幸的是,在运行时很容易检测到这些最新的处理器。
几年前,我们发布了一个名为 simdjson 的非常快速的 C++ JSON 解析器。它作为解析器有些独特,因为它严重依赖 SIMD 指令。在几个指标上,它曾经是并且仍然是最快的 JSON 解析器,尽管已经出现了其他有趣的竞争对手。
最初,我为 simdjson 编写了一个快速而肮脏的 AVX-512 内核。我们从未合并它,一段时间后,我只是将其删除。然后我就忘记了。
感谢有才华的英特尔工程师(Fangzheng Zhang 和 Weiqiang Wan)的贡献,以及本博客读者(Kim Walisch 和 Jatin Bhateja)的间接贡献,我们制作了一个新的 AVX-512 内核。与往常一样,simdjson 是许多人的工作,是一个由数十名贡献者组成的整个社区。我必须对第一次写信给我关于 AVX-512 端口的张方正表示感谢。
我们刚刚发布了最新版本的 simdjson。它打破了新的速度记录。
考虑一个有趣的测试,您试图扫描整个文件(跨越千字节)以找到与某个标识符对应的值。在simdjson中,代码如下:
1 | auto doc = parser.iterate(json); |
在带有 GCC 11 的 Tiger Lake 处理器上,处理速度提高了 60%,以每秒处理的输入字节数表示。
simdjson (512-bit SIMD): new | 7.4 GB/s |
---|---|
simdjson (256-bit SIMD): old | 4.6 GB/s |
速度增益非常重要,因为在这个任务中,我们大多只是读取数据,而我们做的二次处理相对较少。我们不会从 JSON 数据中创建树,也不会创建数据结构。
simdjson 库有一个缩小功能,它只是从输入中去除不必要的空格。也许令人惊讶的是,我们的速度是之前基线的两倍多:
simdjson (512-bit SIMD): new | 12 GB/s |
---|---|
simdjson (256-bit SIMD): old | 4.3 GB/s |
另一个合理的基准是将输入完全解析为具有完全验证的 DOM 树。解析标准 JSON 文件 ( twitter.json
),我获得了近 30% 的收益:
simdjson (512-bit SIMD): new | 3.6 GB/s |
---|---|
simdjson (256-bit SIMD): old | 2.8 GB/s |
虽然 30% 听起来可能并不令人兴奋,但我们是从一个快速的基线开始的。
我们能做得更好吗?肯定的。有许多我们尚未使用的 AVX-512 指令。我们不使用三元布尔运算 ( vpternlog
)。我们没有使用新的强大的 shuffle 函数(例如,vpermt2b
)。我们有一个共同进化的例子:更好的硬件需要新的软件,这反过来又使硬件大放异彩。
当然,要获得这些新优势,您需要具有足够 AVX-512 支持的最新 Intel 处理器,显然,您还需要相对较新的 C++ 处理器。最近的一些笔记本电脑级英特尔处理器不支持 AVX-512,但如果您依赖 AWS 并拥有大型英特尔节点,您应该没问题。
您可以直接获取我们的版本或等待它到达标准包管理器之一(MSYS2、conan、vcpkg、brew、debian、FreeBSD 等)。
]]>在神经网络中,端到端是指不关心中间过程,直接从输入到输出。
在通信领域中,端到端是指从请求节点到访问节点,譬如从手机App端到服务器端。有一个名词叫做“端到端加密”就是指从发送方到接收方全流程是包裹加密的,在这种场景下,流程中的任何一点泄密都会导致端到端加密失效。
在公司的场景中,端到端更多指的是从需求出发到交付,它强调的是流程打通,部门的协同。假设有一个公司有 市场、采购、生产、销售 四个部门。市场部做好调研,采购买好原材料,生产部门高质量生产,销售部门努力推销,全流程打通满足用户的需求,就算是完成了一个端到端。其中任何一个部门没有完成好自己的职责(生产工艺不合格),或者是部门之间的协同出现问题(采购生产对接失败,采购型号不符合生产需求),都会使得端到端失败。
端到端的概念本身并不复杂,但是它可以给人一些启示。尤其是长期深入挖掘到技术细节的同学,有可能会忘了出发的初心。此时我们需要回过神来,结合一些系统性思考,再溯源一下做事的本因:当前做的事情是否可以更好地支持最原始的需求。通过这种方式,我们可以调整方向,减少人力资源的浪费。
]]>
那么什么是超复杂呢?最开始的时候,很多团队可能都采用单体架构,随着业务演进、团队扩充,我们需要对服务进行逐步拆分。因此随着业务变得复杂,我们的调用链、调用网也会变得越来越复杂。当它们复杂到一定的程度时,很多难缠的问题就出现了。
当前很多团队在进行微服务化的过程中,可能暂时仅看到微服务的优势,未遇到服务管理上的问题,毕竟不是每一套系统都达到了超复杂的标准,但是提前关注这些问题并做好预案也非常重要。作为企业的软件架构师或是技术负责人,我们应当始终用发展的眼光看问题,软件行业的发展变化非常巨大,如果企业当下的架构无法适应未来一到两年的业务发展,那会对业务和技术进步形成巨大阻碍。如果架构师能吸取其他企业的教训和经验,提前布局,那么业务在扩张过程中遇到的技术问题会少很多。
超复杂调用网带来的难题
我个人对超复杂调用网给出一个定义:
内网非测试的微服务达 1000 个以上
至少存在一个微服务,且其实例数达到 300 个以上
对外 API 普遍涉及至少 10 个微服务
在内部技术实践中,我们发现系统达到这个量级后,超复杂调用网就会产生许多棘手的问题。
第一个要点是微服务的数量。如果一个系统内的微服务数目只有几百个,那么绘制一张囊括所有微服务的调用图是有利于管理的;但如果超过了 1000 个,再把它们塞到一张图后整张图变得不可读,它的意义就不大了。
第二点,如果一个微服务的实例数只有几十个,这时实例的管理是比较简单的,如果实例数超过 300,那么团队不可避免地会需要使用一些分片策略或是长连接策略,它们都会带来一些特殊问题。
第三点是单个 API 涉及的微服务数量。如果 API 需要普遍涉及 10 个以上的服务,这时监控会面临更大的挑战。以字节跳动的场景为例,目前字节跳动内网的在线微服务数量在万级,其中最大的微服务大约有 1-2 万个实例,而单个 API 也普遍在后端关联了几十个甚至上百个微服务。面对这样的复杂度,有三个问题最为突出:
一是难以做容量预估。微服务已经达到了一定的复杂度,它们的调用关系是非常复杂的:一个核心服务的依赖链可能就有几百个,对每个依赖方做调研或去细致地跟进每个限流策略显然非常困难。另外,不同业务会通过不同活动实现业务增长,对核心服务来说,追溯每个业务的增长也是一个非常艰巨的任务。
二是会大幅提高服务治理难度。这里的服务治理包含限流、ACL 白名单、超时配置等,因为调用关系变得复杂,每个服务可能会调用几十个甚至上百个依赖服务,一些核心服务也会被几百个服务所依赖,这时如何梳理这些调用关系、配置多少限流、配置怎样的白名单策略,就成了团队需要深度探讨的问题。
三是容灾复杂度增大。在复杂的调用关系下,每个 API 会依赖大量的微服务,而每一个微服务都有一定概率产生故障。我们需要区分强依赖和弱依赖,并辅以特定的降级策略,才能够在不稳定的服务环境下获得尽可能稳定的对外效果。
业界尝试
那么对于这些复杂的治理难题,业界会有怎样的尝试呢?
第一种方式是鸵鸟心态。完全不做工作,这反而是业界最广泛的尝试。相信很多企业并不是没有受到超大规模调用网的侵扰,也不是没有对其做一些尝试,而是解决问题所产生的成本和损失实在是难以量化。
举个例子,一个核心服务有很多依赖方,其中一个依赖方的代码中存在严重的重试漏洞,瞬间产生大量重试把核心服务给压垮了,最终造成了系统级的灾难。这时我们可以去追溯问题的直接原因——代码质量问题,至于隔离没做好、超复杂调用关系没有梳理清楚等,这些会被归结为间接原因,往往可以不被追究。
第二种方式是精细化的监测与限流。业内一些开源组件在功能上确实做得比较出色。如左图是一个知名开源组件,它会对整个服务链路进行精细化监控。在这个示例里,每个三角形是一个 Gateway,中空圆形才真正的服务。它展示了从流量入口到每个微服务的整个链路,如果链路是绿色的,说明流量是健康的;链路是红色的,就说明流量存在异常。有了这样详细的拓扑图,开发者就可以看清它的依赖关系。
这看起来很美好,所以大概在两年前,我选取了一个中等规模的业务线,把所有依赖关系梳理出来,得到了上图中右侧这张图。里面每一个代号都是一个服务,每一条线都是这个服务的依赖关系——这实在是太复杂了。左图由于只有 4 个服务,整体比较清晰,但如果是几百个服务相互交织、相互依赖,用这种图来进行测算无疑是不可行的。
第三种方式是单元化,或称 SET 化,比较有代表性的是蚂蚁和美团。他们采用的主要方式是把每一个服务部署多份:set 1、set 2、set 3,流量通过单一的 shard key 进行 set 的选择。这样,set 之间就可以进行有效的资源隔离,在单个 set 产生问题时可以通过切流的方式容灾。
但它也有三方面的局限性。第一方面,SET 化需要有合适的分片键,如用地域或账号去切分,这需要和业务属性有匹配,并不是所有的业务都能找到这种合适的分片键。第二方面,这种方式需要的非全局数据比较多,譬如本地生活订单,用户在北京下单酒店的数据没必要经过深圳。但在抖音、今日头条这些综合信息服务场景中,非全局数据非常少,那些看似本地的数据如用户名、用户的粉丝数、近期的点赞列表,其实也是全局数据。最后一个方面,SET 化需要冗余,需要备份成本,大体量的公司不一定能够支撑。
第四种方式是 DOMA。它的英文全称是 Domain-Oriented Microservice Architecture。2020 年,Uber 提出了这个架构。下图是一个简单示例,其中绿色是 public interface,红色的是 private interface。如果有流量想访问域内的一个微服务,它必须要经过 Gateway Service 进行转发,然后才能访问。
如果用户想要在域外访问这个数据库,我们需要通过左下角的 Query、ETL 把它转化成一个离线数据库。整个大框是一个 domain,它不同于 DDD 的 domain,它被称为服务域,可以理解成是一组服务的集合。字节跳动内部也参考了这种 domain 的思想,把一些服务聚合起来,产生特殊的化学反应。
但 DOMA 架构也存在一些问题,比如它过了一层 Gateway Service。我们在外层其实已经有一个从外网到内网的 Gateway,如果内网再放置过多 Gateway(尤其是中心化的),肯定会带来额外的性能消耗,并造成一定的延迟上涨,这也是字节跳动没有采取这种方式的原因。
字节跳动的探索和实践
对于超复杂调用网,字节跳动探索出了一些最佳实践,其中第一个核心叫做服务分层原则。
正如前文的微服务架构图所示,服务在经历从上到下的调用后出现了很复杂的调用关系,对此,我们可以依据康威定律对它做一些横向切分,对调用关系进行分层。
康威定律是马尔文·康威于 1967 年提出的,指的是设计系统的架构受制于产生这些设计组织的沟通结构。举个例子,假设某家公司内部有四个团队,如上图所示,左侧团队和上方团队沟通较密切,上方团队和下方团队沟通较少,把这种关系映射到微服务架构中后也是类似的,上方微服务和左侧微服务的通信耦合性会大一些,和下方微服务的联系就会弱一些。
我们之前讨论过一个悖论:为什么企业的组织架构非常清晰,但是微服务设计就非常复杂?最终得出的结论是没有做好映射。字节跳动内部有很多团队分别负责业务、中台、基础架构等技术领域,在真实的微服务架构下,我们应该把它清晰地切分成不同层次。
如下图所示,首先是网关层。外网到内网之间需要有一个 Gateway 来处理一些基本事项,如参数基础校验、session 机制、协议转换等。
第二层是 BFF 层。BFF 是近几年日趋流行的一个概念,全称是 Backend For Frontend(服务于前端的后端)。如过一个接口的对外主体业务逻辑是一致的,但在 iOS、Android、Web 等不同客户端的可能有一些细微差别,那么这些差别可以放在 BFF 层处理。
第三层是业务层。字节跳动有很多业务,如短视频、资讯、游戏、公益等,与特异业务功能直接相关的功能应当由这一层来实现。
第四层是中台层,这一层应用了 DDD 的思想,我们抽取了一些通用的特殊能力,对它们进行专业化的建模和封装,以实现大量基础能力的复用。
第五层是数据服务层,通过合理的封装,用户无需直接访问数据库的表即可更方便、更安全地使用数据。
最后一层是基础架构层,这层主要提供基础架构领域的各种能力,比如微服务基础组件、微服务基础依赖以及数据库或是消息队列等。
字节跳动之所以可以快速孵化新产品,业务层和中台层的建设是一个重要原因。比如新做一个教育应用,我们可以直接调用成熟的账号系统、支付系统、直播模块等,也可以通过向学员推送他可能感兴趣的视频,将他们转化成付费会员。由于存在这类专业领域的建模,在对微服务进行归类处理时,分层变得尤为重要。
这里有几个指导思想供大家参考:首先是分层原则需要结合业务灵活调整,DDD 只是一种指导思想,不能按照它的每一条规范去做;其次是在分层原则中,建议从上到下去进行访问,业务层的请求可以访问数据服务层,但数据服务层的请求不能访问中台层,逆向访问可能会产生循环依赖等严重问题;第三,对于调用关系异常复杂的业务层、中台层,我们给出了一种点线面结合的方法:
点:流量身份标记注入点
线 1:流量身份标记沿调用链透传
面:紧耦合的服务聚合为服务域
线 2:部署和流量按域切分
点在字节跳动内部被称为流量身份标记 TIM(Traffic Identity Mark)。流量从客户端进来后,我们会在 Gateway 层对 request 的各种参数进行检测,验证之后,一些需要在链路中传递的核心参数会被记录下来,供后续分流、核心服务调用使用。
这种做法有助于一些特殊链路数据保护策略的实现,如未成年人数据保护。未成年人发出的请求从一开始就带有相关参数,随着调用链向下传递,通过透传机制,核心的中台层和数据服务层依然能读到这些信息,并执行特殊的逻辑,以便对未成年人做好保护。
有了点之后,如果想在下游核心业务中使用这些关键信息,就必须要求信息会向下透传。举个例子,假设抖音的一个请求带有流量身份标记 TIM1,那么该流量触达下游服务时仍应携带标记 TIM1;如果流量来自西瓜视频且携带了 TIM2,那么由这个请求触发下一个在线请求时,它也一定要携带这个 TIM2。这使得整个调用链可以完成串联,类似 Log ID、Trace ID。
所以这个地方有两个依赖,我们最好把 TIM 放在 Header 中,让它能更好地传递信息,并且使下游服务在不解析它的请求 Body 时,就能拿到 Header 中的信息来做流量调度等操作。在一个微服务内部,我们要通过 Context 机制,把入流量和出流量结合起来,把真正的标记传递过去,形成链路。
在字节跳动,“面”是指高内聚的服务要聚合成服务域。上文介绍过康威定律,即软件架构受制于组织沟通架构:如果有一组服务,它们的合作和联系非常紧密,相互调度非常多,但是共同对外暴露的功能点又比较少,那么我们就可以把它们聚合为一个服务域。
通过自动搜索流量的紧密、松散程度,结合组织架构关系,我们可以为内部开发者提供服务域自动推荐,但最终设计还是需要服务维护人员进行确认。确定服务域后,服务之间的关系也真正确定下来。紧耦合的服务也需要采用同样的治理策略。
“线 2”有两层含义,一是域管理员自行决定部署策略,二是要根据目标服务域按条件分流。
如上图所示,服务域 A 是一个业务,它的域管理员希望按地域进行切流,把南方的服务调度到左边,把北方的服务调度到右边,他可以自由选择调度的策略。
服务域 C 是一个核心中台服务,比如评论服务,它不应当按照地域进行划分,而是按照 User ID 进行流量划分。基于这个目标,域管理员希望服务域可以按照 ID 取模进行切分,这也是可以的。在服务域内,它就可以形成这样一条泳道,流量可以在泳道中向下传递。
对于服务域之间的流量,在域管理员确定部署策略之后,它会根据目标服务域的调度策略进行分流。举个例子,如果服务域 A 想去访问服务域 C 中的某个服务,流量从 A 出来后,它会根据 C 的切流方式进行切流。字节跳动的绝大多数在线流量已经接入 Service Mesh,我们能够动态分析目标服务的部署策略、切流策略,并反馈给 Client 所在的 mesh proxy,Client mesh proxy 会动态修改目标服务的集群,把流量打到目标集群上去。
当然 Mesh 只是一种方法,开发者也可以用框架或业务代码实现同样的效果,但如果有企业和组织正在内部推广 Service Mesh,上述提到的流量透传、流量注入、根据目标部署情况动态按条件分流等都可以提前放在系统和框架中进行考虑。
在 2021 年抖音央视春晚红包活动中,这套超复杂调用网服务治理思路也有充分应用。活动往往意味着流量激增,容灾测试、全链路压测、容量预估,我们遇到了不少难题。有了这个切流方案后,我们最终较理想地把服务域都找了出来,最终在活动上线后保障了流量的稳定分发,且没有对其他业务造成影响。
结语
目前,字节跳动正面临超复杂调用网治理的严峻挑战,它带来的问题是实实在在的。我也相信,随着国内企业的不断发展,很多公司未来也会发展到调用网极其复杂的境地,需要直面同样的问题。为了帮助业务实现健康过渡,大家最好能够做两个布局:
第一个布局是把服务分层做得足够好。可以参考字节跳动的方案,按照分层原则排布服务,使各个组件能够充分发挥优势。
第二个布局是梳理调用链。这一点同样可以参考我们点线面的实践,根据可信的流量标记动态调配流量。
如果这两个布局都能够做好,那么开发者既可以享受微服务的优势,同时也能尽量规避微服务带来的复杂度。
]]>那么什么是超复杂呢?最开始的时候,很多团队可能都采用单体架构,随着业务演进、团队扩充,我们需要对服务进行逐步拆分。因此随着业务变得复杂,我们的调用链、调用网也会变得越来越复杂。当它们复杂到一定的程度时,很多难缠的问题就出现了。
当前很多团队在进行微服务化的过程中,可能暂时仅看到微服务的优势,未遇到服务管理上的问题,毕竟不是每一套系统都达到了超复杂的标准,但是提前关注这些问题并做好预案也非常重要。作为企业的软件架构师或是技术负责人,我们应当始终用发展的眼光看问题,软件行业的发展变化非常巨大,如果企业当下的架构无法适应未来一到两年的业务发展,那会对业务和技术进步形成巨大阻碍。如果架构师能吸取其他企业的教训和经验,提前布局,那么业务在扩张过程中遇到的技术问题会少很多。
超复杂调用网带来的难题
我个人对超复杂调用网给出一个定义:
这看起来很美好,所以大概在两年前,我选取了一个中等规模的业务线,把所有依赖关系梳理出来,得到了上图中右侧这张图。里面每一个代号都是一个服务,每一条线都是这个服务的依赖关系——这实在是太复杂了。左图由于只有 4 个服务,整体比较清晰,但如果是几百个服务相互交织、相互依赖,用这种图来进行测算无疑是不可行的。
第三种方式是单元化,或称 SET 化,比较有代表性的是蚂蚁和美团。他们采用的主要方式是把每一个服务部署多份:set 1、set 2、set 3,流量通过单一的 shard key 进行 set 的选择。这样,set 之间就可以进行有效的资源隔离,在单个 set 产生问题时可以通过切流的方式容灾。
但它也有三方面的局限性。第一方面,SET化需要有合适的分片键,如用地域或账号去切分,这需要和业务属性有匹配,并不是所有的业务都能找到这种合适的分片键。第二方面,这种方式需要的非全局数据比较多,譬如本地生活订单,用户在北京下单酒店的数据没必要经过深圳,用户在上海点外卖的数据也不需要经过。但在抖音、今日头条这些综合信息服务场景中,非全局数据非常少,那些看似本地的数据如用户名、用户的粉丝数、近期的点赞列表,其实也是全局数据。最后一个方面,SET 化需要冗余,需要备份成本,大体量的公司不一定能够支撑。
第四种方式是 DOMA。它的英文全称是 Domain-Oriented Microservice Architecture。2020 年,Uber 提出了这个架构。下图是一个简单示例,其中绿色是 public interface,红色的是 private interface。如果有流量想访问域内的一个微服务,它必须要经过 Gateway Service 进行转发,然后才能访问。
如果用户想要在域外访问这个数据库,我们需要通过左下角的 Query、ETL 把它转化成一个离线数据库。整个大框是一个 domain,它不同于 DDD 的 domain,它被称为服务域,可以理解成是一组服务的集合。字节跳动内部也参考了这种 domain 的思想,把一些服务聚合起来,产生特殊的化学反应。
但 DOMA 架构也存在一些问题,比如它过了一层 Gateway Service。我们在外层其实已经有一个从外网到内网的 Gateway,如果内网再放置过多 Gateway(尤其是中心化的),肯定会带来额外的性能消耗,并造成一定的延迟上涨,这也是字节跳动没有采取这种方式的原因。
字节跳动的探索和实践
对于超复杂调用网,字节跳动探索出了一些最佳实践,其中第一个核心叫做服务分层原则。
正如前文的微服务架构图所示,服务在经历从上到下的调用后出现了很复杂的调用关系,对此,我们可以依据康威定律对它做一些横向切分,对调用关系进行分层。
康威定律是马尔文·康威于 1967 年提出的,指的是设计系统的架构受制于产生这些设计组织的沟通结构。举个例子,假设某家公司内部有四个团队,如上图所示,左侧团队和上方团队沟通较密切,上方团队和下方团队沟通较少,把这种关系映射到微服务架构中后也是类似的,上方微服务和左侧微服务的通信耦合性会大一些,和下方微服务的联系就会弱一些。
我们之前讨论过一个悖论:为什么企业的组织架构非常清晰,但是微服务设计就非常复杂?最终得出的结论是没有做好映射。字节跳动内部有很多团队分别负责业务、中台、基础架构等技术领域,在真实的微服务架构下,我们应该把它清晰地切分成不同层次。
如下图所示,首先是网关层。外网到内网之间需要有一个 Gateway 来处理一些基本事项,如参数基础校验、session 机制、协议转换等。
第二层是 BFF 层。BFF 是近几年日趋流行的一个概念,全称是 Backend For Frontend(服务于前端的后端)。如过一个接口的对外主体业务逻辑是一致的,但在 iOS、Android、Web 等不同客户端的可能有一些细微差别,那么这些差别可以放在 BFF 层处理。
第三层是业务层。字节跳动有很多业务,如短视频、资讯、游戏、公益等,与特异业务功能直接相关的功能应当由这一层来实现。
第四层是中台层,这一层应用了 DDD 的思想,我们抽取了一些通用的特殊能力,对它们进行专业化的建模和封装,以实现大量基础能力的复用。
第五层是数据服务层,通过合理的封装,用户无需直接访问数据库的表即可更方便、更安全地使用数据。
最后一层是基础架构层,这层主要提供基础架构领域的各种能力,比如微服务基础组件、微服务基础依赖以及数据库或是消息队列等。
字节跳动之所以可以快速孵化新产品,业务层和中台层的建设是一个重要原因。比如新做一个教育应用,我们可以直接调用成熟的账号系统、支付系统、直播模块等,也可以通过向学员推送他可能感兴趣的视频,将他们转化成付费会员。由于存在这类专业领域的建模,在对微服务进行归类处理时,分层变得尤为重要。
这里有几个指导思想供大家参考:首先是分层原则需要结合业务灵活调整,DDD 只是一种指导思想,不能按照它的每一条规范去做;其次是在分层原则中,建议从上到下去进行访问,业务层的请求可以访问数据服务层,但数据服务层的请求不能访问中台层,逆向访问可能会产生循环依赖等严重问题;第三,对于调用关系异常复杂的业务层、中台层,我们给出了一种点线面结合的方法:
点在字节跳动内部被称为流量身份标记 TIM(Traffic Identity Mark)。流量从客户端进来后,我们会在 Gateway 层对 request 的各种参数进行检测,验证之后,一些需要在链路中传递的核心参数会被记录下来,供后续分流、核心服务调用使用。
这种做法有助于一些特殊链路数据保护策略的实现,如未成年人数据保护。未成年人发出的请求从一开始就带有相关参数,随着调用链向下传递,通过透传机制,核心的中台层和数据服务层依然能读到这些信息,并执行特殊的逻辑,以便对未成年人做好保护。
有了点之后,如果想在下游核心业务中使用这些关键信息,就必须要求信息会向下透传。举个例子,假设抖音的一个请求带有流量身份标记 TIM1,到更下游的时候,如果流量还是源于抖音,那么还是 TIM1,如果流量来自西瓜视频且携带了 TIM2,那么由这个请求触发下一个在线请求时,它也一定要携带这个 TIM2,这使得我们整个调用链是能够串起来的,类似 Log ID、Trace ID。
所以这个地方有两个依赖,我们最好把 TIM 放在 Header 中,让它能更好地传递信息,并且使下游服务在不解析它的请求 Body 时,就能拿到 Header 中的信息来做流量调度等操作。在一个微服务内部,我们要通过 Context 机制,把入流量和出流量结合起来,把真正的标记传递过去,形成链路。
在字节跳动,“面”是指高内聚的服务要聚合成服务域。上文介绍过康威定律,即软件架构受制于组织沟通架构:如果有一组服务,它们的合作和联系非常紧密,相互调度非常多,但是共同对外暴露的功能点又比较少,那么我们就可以把它们聚合为一个服务域。
通过自动搜索流量的紧密、松散程度,结合组织架构关系,我们可以为内部开发者提供服务域自动推荐,但最终设计还是需要服务维护人员进行确认。确定服务域后,服务之间的关系也真正确定下来。紧耦合的服务也需要采用同样的治理的策略。
“线 2”有两层含义,一是域管理员自行决定部署策略,二是要根据目标服务域按条件分流。
如上图所示,服务域 A 是一个业务,它的域管理员希望按地域进行切流,把南方的服务调度到左边,把北方的服务调度到右边,他可以自由选择调度的策略。
服务域 C 是一个核心中台服务,比如评论服务,它不希望按照地域进行划分,而是按照 User ID 进行流量划分。基于这个目标,他希望服务域可以按照 ID 取模进行切分,这也是可以的。在服务域内,它就可以形成这样一条泳道,流量可以在泳道中向下传递。
对于服务域之间的流量,在域管理员确定部署策略之后,它会根据目标服务域的调度策略进行分流。举个例子,如果服务域 A 想去访问服务域 C 中的某个服务,流量从 A 出来后,它会根据 C 的切流方式进行切流。字节跳动的绝大多数在线流量已经接入 Service Mesh,我们能够动态分析目标服务的部署策略、切流策略,并反馈给 Client 所在的 mesh proxy,Client mesh proxy 会动态修改目标服务的集群,把流量打到目标集群上去。
当然 Mesh 只是一种方法,开发者也可以用框架或业务代码实现同样的效果,但如果有企业和组织正在内部推广 Service Mesh,上述提到的流量透传、流量注入、根据目标部署情况动态按条件分流等都可以提前放在系统和框架中进行考虑。
在 2021 年抖音央视春晚红包活动中,这套超复杂调用网服务治理思路也有充分应用。活动往往意味着流量激增,容灾测试、全链路压测、容量预估,我们遇到了不少难题。有了这个切流方案后,我们最终较理想地把服务域都找了出来,最终在活动上线后保障了流量的稳定分发,且没有对其他业务服务造成影响。
结语
目前,字节跳动正面临超复杂调用网治理的严峻挑战,它带来的问题是实实在在的。我也相信,随着国内企业的不断发展,很多公司未来也会发展到调用网极其复杂的境地,需要直面同样的问题。为了帮助业务实现健康过渡,大家最好能够做两个布局:
在此次认证中,火山引擎服务网络解决方案全项满分,总分并列第一。依托于网格分布式隔离架构,火山引擎服务网络解决方案能够有效杜绝中心化服务模式产生的系统性风险,并通过细粒度的服务拆分方式与去中心化架构设计,适应当下“云时代”的敏捷开发、快速迭代需求。据悉,该解决方案在业界处于领先地位,其优势包括全功能、多场景、稳定性、高性能等。
首批通过可信云服务网格解决方案国家级标准认证,标志着火山引擎自身服务网格解决方案在服务网格控制层面的技术能力、服务网格数据层面的技术能力、服务网格安全能力、服务网格的运维能力、服务网格性能要求等方面的技术指标,均满足可信云服务网格平台先进级的要求。
此外,在7月28日的可信云大会现场,火山引擎金融行业客户——国信证券凭借蜂鸟容器云平台获颁可信云用户最佳实践。该平台搭载的是火山引擎智能容器云veCompass,适配了国信证券内部多个IT管理系统与微服务架构,构建了一套用于金融行业内部研发运营一体化体系的云原生容器管理平台。veCompass的存在满足了国信证券内部业务系统快速迭代上线的需求,帮助其技术人员快速构建技术运营PaaS和DevOps体系。
平台上线后,蜂鸟容器云平台为国信证券大大节省了硬件投资,降低了运维工作量,全面提升IT资源的服务效能和管理效能。
今年5月,火山引擎智能容器云veCompass也以满分通过开发测试场景、CI/CD、运维自动化、微服务四大应用场景 42 个子项的测试,获得最新版本的可信云容器解决方案认证。至此,火山引擎在云原生、微服务等方面的技术力和产品力,均已获得“可信云”认证。
未来,火山引擎将充分发挥在云原生、微服务等领域的技术能力和产品能力,提供更完善的解决方案,服务好更多的行业客户。
]]>1 | $ bazel |
导致编译过程受阻,非常麻烦。
出现这个问题的原因是:
1 | curl https://api.github.com/repos/bazelbuild/bazel/releases |
说白了就是你和别人共用了 100.101.102.103
作为共用的出口代理,这个出口命中限流了。
登陆 github
点击 Generate-now-token 按钮,生成一个新的 token,譬如起名bazel
check box不用选,这个地方不需要什么权限。
把 export BAZELISK_GITHUB_TOKEN=37493ba********f32d4c4
添加到你的 .bash_profile
文件结尾:
1 | echo 'export BAZELISK_GITHUB_TOKEN=37493ba********f32d4c4' >> ~/.bash_profile |
然后就好了。
1 | bazel version |
一直以来,我们接受的教育都会教导我们,把问题拆解开来,把事情拆细,分而治之。诚然,这卓有成效。对于树状的组织人员结构来说,每个分支、每个成员都有自己负责的部分,每个分支把负责的部分做好,最终整合起来,整体就做好了。划分细致之后,各个细节也都会变得清晰,便于具体追踪。然而,太多地使用拆解之后,我们往往忘记了系统性思考,从整体的角度仔细揣摩,这也造成了很多问题。
分而治之,每个组件处理自己的事情,组件之间减少耦合,这是多数从业者都理解的主题。不过组件之间的问题也存在转移的可能性,如果不注意,一个分支的成就可能成为整个系统的噩梦。
单纯地为了减少获取新配置的延迟,而增加轮询的频率,那么目的肯定是达到了,而代价是配置中心的访问量大大增加了,获取配置的错误率也可能上升,这使得获取新配置的不稳定性增加,系统的不稳定性增加。
为了尽快完成新产品的推广,在没有完全了解可能隐含的问题情况下,强行增加上线的量。这会导致使用方发现很多问题,SRE的工单堆积。推广覆盖率的难题,转化为问题排查和解决的难题。
当然这两个例子比较简单,而且你可能认为这是显然,很容易避免。这是因为轮询组件和配置中心可能是同一个人在维护,问题的转移出现在一个小型的系统内部容易定位;Dev团队和SRE团队之间的交流可能比较通畅,工单的量很好量化。而如果这种问题的转移出现在更大的系统内呢?如果问题从一个明确的可度量的问题,变成一个模糊的无法度量的问题呢?
理论化的代码只存在于理论之中。
为了工程,为了真正把内容应用于生产,我们可能不得不向代码中塞入一些不太优雅的Hack成分。所谓的 hack fix,大多是指对特殊情况最特殊判断,然后避开问题的一个方式。
当系统出现一个问题,打补丁往往是最快速最容易解决的,但这也是最危险的。
当hack的代码变多,整体思考就会变得困难,甚至有可能遗漏hack的情况。在对另外一个组件进行修改的过程中,可能导致此处hack造成严重问题。
所以,尽量减少 hack 代码的量,思考这个问题能否使用更加通用的方式解决;如果不得不这样,务必要在 hack 的代码附近,用注释明确标出原理,以及带来的风险。
在城市天际线这个游戏中,Traffic是最为恼人的问题。当城市的规模发展到一定程度的时候,堵车问题也会一发不可收拾。
道路不断加宽,立体交通不断增进,最终还是拥堵严重。
实质上,问题可能并不在交通,而在于城市区域规划。如果从高速路到工业区必须要经过居民区,那居民区就会有大量的车流;如果工厂、商店过于密集,生产和消费的类型不匹配,进出口需求量大,这本身就会造成巨量的运输压力,无法通过道路交通系统解决。
在问题发生的时候,主动寻求整体最优的解决方案,而不应该在一个组件上耗费太大的精力。在城市规划不合理的时候,必须要优先修改产业布局;在系统规划不合理的时候,务必要优先调整组件排布。磁盘压力过大,网络压力过大,也许是缓存组件没做好。
这是在新功能推广过程中出现的。
耗费了很多精力,吹嘘了很多优势,推广了很多的接入,但突然因为一个不稳定,导致所有接入全部回滚,很长一段时间使用者心有余悸,不愿意主动接入。
组件和组件之间要系统性思考,权衡利弊,追求整体利益;时间维度上,也需要系统性思考。
目标是确定的,在某个时间区间内完成稳定的接入,那么必须在长时间维度上综合考虑,一时的强推很容易造成反弹。
并不打算写一个 ASM 使用流程指南,毕竟 https://help.aliyun.com/document_detail/149552.html 才是更为专业的文档。
点击了 服务网格 ASM 的链接之后,映入眼帘的是错误提示
Aliyun API Error: RequestId: 08147C2C-9EE9-XXXXXX Status Code: 404 Code: EntityNotExist. Role Message: The role not exists: acs:ram::931204812349238:role/aliyuncsdefaultrole.
重试了一下,终于进来了,但是整个页面什么都没有:
最后发现服务公测中,还需要独立申请才可以使用,所以没有办法,还是先看一下文档吧。
所谓的功能特性,应当和其它的服务网格产品差不多。
都是把服务网格划分为控制平面和数据平面。
ASM的优势在于兼容Istio,估计是在 Istio 的基础上,增加了支持 aliyun PaaS基础架构的内容。
从部署上看,依然是支持了混合云的环境,可以支持 Kubernetes集群、ServerLess集群和ECS虚拟机的混合部署。Proxy 与具体的服务进程在一起分享资源,并代理 Service 的出入流量。
共性的内容就不提了,既然作为公有云的一个主打产品,它的主要贡献应当在与组件的托管,可以通过简单的配置,就可以搭建起一整套服务网格,而不需要用户担心环境和配置的问题。
目前来看ASM除了托管之外,其实没有什么额外的优势。相应的功能,包括流量路由、安全、监控都是社区版本已经提供的内容。高稳定性和高可用性,由于该产品本身就是公测版,所以暂且无法证实。
对于中小型厂商来说,没有足够的精力去维持一个团队,专门做一些细节的事情,譬如分5个人的团队来做混沌工程,7个人做可观测性、链路追踪,或者8个人的团队专门做RBAC的维护、mTLS的推进,因此ASM确实还是有一定的吸引力的。
应用场景就是把优势又冗长地说了一遍。
有些内容务必在网格实例刚刚创立的时候,就配置好。譬如
变更网格所依赖的 VPC 与虚拟交换机
如果创建时未开启公网暴露 API Server,暂不支持增加公网 SLB 暴露 API Server
如果创建时未开启公网暴露 Istio Pilot,暂不支持增加公网 SLB 暴露 Istio Pilot
变更链路追踪服务配置
这些确实说明,在动态配置方面,PaaS做得还没有足够成熟。
不过这些问题,都可以用时间磨平,并不是太深层次的技术问题。
但是关于配额,目前还是有些夸张。
每个用户的可创建网格实例数:2
每个网格的Envoy代理数:建议200以下,超过 200 可能会造成网格实例的不稳定。
关于这个实例数,确实还是太小了。如果只是为了200个实例,那么确实控制面内部不需要做什么优化,主要是功能性的开发,而不用太关心性能的问题。
而且,适用场景上来看,ASM只能应对中小型企业的使用场景,在这个200个Envoy实例这个量级下,估计接口的QPS无法超过10万,一般只能适用于100万日活跃用户左右的App来使用。要将代理的规模突破一万,甚至一百万,可能还需要很多的努力。
根据文档介绍,修改目标规则需要:
在控制平面区域的目标规则页签,找到待修改的目标规则,在操作列中单击YAML。
在编辑实例页面,修改目标规则,单击确定。
从这个介绍来看,控制平面的规则修改没有做到平台化,主要仍是通过配置文件的方式来进行管理。不过由于是社区版本,所以配置文件的格式应当与社区是一致的,这给有社区经验的配置者提供了一定的好处。
公测版目前免费,未来还不确定。
由于 Service Mesh 目前的社区版还主要是面向中小型企业,而且很多都是尝试性地使用,所以估计不会有太高的定价。中小用户使用服务网格更多意义上是一个尝鲜,恐怕把主要业务都移动上来需要很大的勇气,暂时不看好两年以内的服务网格公有云市场。
确实没有更多额外的信息了,等我的公测申请通过了,再来同步吧。
]]>首先 RAM 授权 ASM 的访问权限。
创建网格。
创建的过程中需要填写 专有网络,还要创建专有网络。
对于微服务领域,也有一些词汇,其狭义上和广义上范畴不同的问题。
譬如服务发现,如果一个人说他是做服务发现的话,他基本上也会去做服务注册。服务发现广义上是包含服务注册的。
同样的,流量染色在广义上,应该包括无色流量的浸染过程,以及染色后的流量的处理过程。
继Service Mesh之后,又逐渐迸发了很多诸如 DB Mesh、MQ Mesh、General-Purpose Mesh 等新的Mesh,那么实际上广义上的 Service Mesh 也可以涵盖后续一些新的名词。
在国家演进的过程中,很可能出现荷兰这种名称不匹配的问题;
英国尚且可以通过“大不列颠”来代替英格兰,作为整个帝国的名称,但是绝大多数情况下是没有办法的。
技术演进的过程中,也会有一个相对片面的名词来代指整个事情的状况。这种情况其实是很难改变的,只要选择接受就好。
更多时候,不纠结反而有利于技术的交流和宣传,不要拘泥于绝对的严谨。
改名实在是一件非常困难的事情。如果想定一个名字,尽量刚开始就想清楚吧。
]]>EDF调度算法,是加权轮转调度算法(WRR,Weighted Round-Robin)的一种实现方式。
其核心思想是为每个条目截止时间赋值为当前时间加权重的倒数,然后采用最早截止时间优先的方式进行调度。
上述定义比较晦涩,我们可以透过后面例子,来说明为什么需要调度算法、如何实现调度算法、EDF调度算法的优势和劣势。
调度算法最主要的应用是操作系统调度进程,重要的调度理论基本上都是在此时涌现的。而后续反向代理对下游条目进行负载均衡,也可以参考一样的调度理论,只是进程的运行和切换转变为请求的接受与投递。
微服务架构下,请求方会获得一个服务节点列表,对服务节点的访问也可以利用同样一套负载均衡算法。它们大部分内容是可互通的,而访问负载均衡又会额外衍生出如一致性哈希等新的调度方式。
假设有三个条目可供调度,分别是A、B、C,他们的权重分别是3:2:1。
设置权重的目标是相关条目被调度到的频次应该是与权重成正比。
对于这个权重来说,假设有6000次调度,那么我们希望A被调度3000次左右,B是2000次左右,C是1000次左右。
为了实现这种调度方式,我绘制了一个图,来解释这种方法:
图上有一个数轴,从0开始,到达3,以至无穷。
A条目的权重是3,我们以1/3为分隔不断重绘A,使得数轴的1/3,2/3,1,4/3等位置印上A;
B条目的权重是2,我们以1/2为分隔不断重绘B,使得数轴的1/2,1,3/2,2等位置印上B;
C条目的权重是1,我们以1为分隔不断重绘C,使得数轴的1,2,3等位置印上C;
最后,我们使用一个游标从左向右扫,扫描到的顺序就是调度的顺序,因此我们调度的顺序为A-B-A-C-B-A-A-B-A…
显而易见,调用的顺序含有一个循环节A-B-A-C-B-A,所以当调度足够多次数后,A、B、C的调度比值将会趋近于3:2:1
从数学上也很好证明。
假设有N个条目,其权重分别是a1,a2,a3…aN。
使用权重不停重绘,那么在一个周期内,第一个条目被重绘a1次,第二个条目被重绘a2次,第N个条目被重绘aN次。
因此,每个周期的调度都是按照目标比例的。
由于采用倒数进行重复,相当于对条目进行了穿插,这样可以减少了连续调度,降低了饥饿和过载的概率。
A-B-A-C-B-A-A-B-A-C-B-A的调度效果一般要比A-A-A-B-B-C-A-A-A-B-B-C更好。
最简单的实现,是使用上述办法模拟一个周期,然后把周期存到数组中,用游标扫描即可。
以上述情形为例,首先我们定制一个数组 A-B-A-C-B-A,然后不断回环扫描这个数组,就可以完成加权轮转调度。
这种方法的每次调度的时间复杂度为O(1),空间复杂度为O(M*N),其中M是条目的平均权重,N是条目的数量。
分析一下这个实现的优劣:
优势:
对于条目平均权重和条目总数比较小的情形下,这种实现其实已经比较优秀了。但是当平均权重和条目总量非常多的情形下,这种方式未免太耗内存。
在我的实际经验中,N可能超过5k,而M经常也在50左右,那么为了实现加权轮转,相当于我们需要耗费250K*Sizeof(Entry)的空间来做调度。即使Entry使用指针,64位系统下每个指针占据4字节,1M的内存就这么用出去了。
一个系统同时使用多个调度器,且条目数、条目权重还可能随着系统的运行而不断修改。
假设每隔十秒钟修改其中一个条目的权重,那么整个表需要重新构建,这对内存和CPU都是一个很大的考验。
所有的条目信息必须要存储下来,因此对于存储空间来说,O(N)的内存消耗几乎不可避免,所以要想办法减少M带来的消耗。
由于 Weight > 0 且不可能等于 +♾,所以数轴上不会有单点出现多个同样的条目,所以从这个角度来看条目是可以复用的。
从这个思路出发,我们定义以下结构体。
1 | // Entry is an item for load balance |
初始时 entry.deadline = 1.0/entry.Weight
调度的时候,从中选择 deadline 最小的使用,并重新把这个条目放入优先队列中,并把 deadline 设置为 deadline + 1.0/entry.Weight,重新排布优先队列,如此往复。
复杂度分析:
效果参考:
1 | === add entry A(3), B(2), C(1) |
当 deadline 一样的时候,index的作用才能显现出来。先加入的条目应该先出现,这样可以保证一定的稳定性。
否则,对于100个条目权重一模一样的调度来说,如果没有index的存在,那机会就变成随机调度了。
当weight不相同的时候,index也并非总是有效。
譬如 A 的 weight 为 3,C的 weight 为2 的时候,在第八轮,A有可能因deadline为精度问题比C早调度到。
O(N)的内存消耗并非完全不可避免。
在 Service Mesh 场景下, control plane 对交给 proxy 的服务发现列表进行分片,可以使得每个 proxy 获得的条目数量减少。这种方式可以减少存储和Pick的时间消耗,同时还会有优化连接池等其他额外的优势。
关于服务发现分片的问题,未来会出一篇专门的文章详细解释。
1 | // avoid instance flood pressure for the first entry |
新建一个EDF的时候,开始进行了一定的随机,为什么要这么做呢?
假设有一个条目的权重较大,那么不进行随机,第一次调度选举一定会调度到这个条目。
想象这样一个场景,分布式环境下所有的调度器由于某种问题同时重启,那么第一瞬间,所有的调度器都会调度到第一个条目,这很有可能造成第一个条目被瞬间的压力打挂。
Traffic Director可以应用于虚拟机(Virtual Machine),也可以应用于基于Kubernetes管理的容器,它使用Envoy开源的xDS v2 API接口来与数据面的服务代理进行交互。
Traffic Director 官网图例
下面,我会从发布现状、主体结构、主要功能、支持场景和使用体验五个方面讲解Traffic Director。
从发布来看,现状相对来说令人悲观。
在2018年7月推出Alpha版本
在2019年4月推出Beta版本
截止目前(2019-07-01),该功能仍未GA,而且Beta版本涵盖的功能非常有限。
Traffic Director的主体结构
Service Mesh基本结构
这个图是比较基本的Service Mesh架构图。Traffic Director的位置,是充当 Service Mesh Control Plane。
对于数据面,Traffic Director建议使用Lyft公司开源的envoy。当然,一切支持 xDSv2 APIs 的数据面都是可以使用的。
微服务环境下,作为控制面的Traffic Director
全局负载均衡
中心化健康检查
基于Load的自动扩缩容
内嵌的弹性(高可用)
强大的流量控制能力
从功能上,我的一篇翻译来的博客 GCP网络深度解析:Traffic Director 如何提供全局负载均衡 已经讲述地比较清楚了;
原理和意义上,敖老师的《Google Traffic Director 详细介绍》 讲的也非常清晰,因此这里不会讲得特别详细,只会针对一些功能与使用的重点。
从支持来看,Traffic Director目前支持 VM + Pod,这还是令人欣慰的。
你可以在 GCE(Google Compute Engine)和 GKE(Google Kubernetes Engine)上使用。不过从官方的指南来看,Traffic Director只支持自家产品,这是源于Traffic Director在生效的时候会操纵一些Google的API,因而不能直接支持其他的公有云或者私有云。
无论如何,VM+Pod的模式也是顺应了混合云的趋势,控制面不应该和云原生进行太强的绑定,还是要考虑到很多的服务仍有可能部署在虚拟机之中,这也是所有想要把Service Mesh落地的人需要重点考虑的问题。
部署
首先建立一个GKE集群。云原生的集群,使用Traffic Director 应该比VM要更方便一点。
启动样例service,这个时候需要注意的是Traffic Director 只支持手动注入。也就是需要手动修改 kubectl -f 后面的 yaml 文件,让 container 把 Envoy 的容器也加载进同一个Pod内部。未来有可能会支持自动注入。
和其他的教程一样,这些指导性的操作都可以使用 Console 和 Gcloud 分别配置。即同时支持控制台图形界面操作和命令行操作的能力。(其实背后的API是一致的)
部署的体验总体还算容易,不过对于用户来说,需要比较多的GCP操控经验。否则面对很多GCP的概念还是一头雾水。
服务配置
Beta版本的配置能力非常地弱。
事实上,在Beta版本的Traffic Director配置页面中,只有两个Tab,分别是“服务”和“规则”。
什么是服务,理论上 VM/Pod + xDS API = 服务
只要你的VM或者Pod注入了Proxy,拥有了xDS API的能力,就算是一个服务了。当然配置服务的时候可以选择一些额外的配置,譬如端口号、平衡模式、健康检查等等。
服务配置
平衡模式的涵义就是流量速率控制,可以选择RPS(Request Per Second)的阈值,也可以选择CPU的比例。当RPC达到阈值,或者CPU达到比例之后,Traffic Director就不会把流量打过来,而是选择最为临近的可用的其他节点。
因为Traffic Director只能用来导引HTTP的服务,因此健康检查也相对容易一些。一般地,只需要每隔5秒检查一下端口可用性即可。连续两次发现端口不可用,即认为服务不健康。
规则配置
绝大对数流量配置能力还在Alpha阶段,需要单独申请才能试用。
Beta版本下只有一些基础功能,配置界面如下:
路由规则配置
首先,你需要制定一个转发规则,决定目标;
然后你再确定一下主机和路径规则。可以使用主机(Host)、路径(Path)、服务(Service)进行匹配,匹配能力和转发能力都非常差。
Traffic Director 目前还处于测试状态,功能也不全,距离落地仍然很远。对于一个线上应用来说,进行Traffic Director的迁移肯定是得不偿失的。
在官网文档中,作者描述了很多局限性,包括:不支持Istio API、只支持HTTP流量、流量控制还在Alpha测试阶段、暂时与GCP强绑定等等。几乎每个局限性都是生产环境落地的死门。
但无论如何,使用数据面的开放接口并提供一个高可用的托管控制面的思路是非常明确的。在未来的计划中,Traffic Director将会对Istio的功能有更好的支持,提供更强大的流量控制与可观测能力,带来更强的稳定性和灵活性,所以Traffic Director的未来依然值得期待。
相关参考:
《Traffic Director官网文档》https://cloud.google.com/traffic-director/docs/traffic-director-concepts
《GCP网络深度解析:Traffic Director 如何提供全局负载均衡》 http://zablog.me/2019/05/16/2019-05-16/
《Google Traffic Director 详细介绍》 https://skyao.io/post/201905-google-traffic-director-detail/
《Google Cloud networking in depth: How Traffic Director provides global load balancing for open service mesh》https://cloud.google.com/blog/products/networking/traffic-director-global-traffic-management-for-open-service-mesh
服务网格为不同队伍用不同语言开发的独立微服务提供了基础。服务网格将开发和运维进行解耦,开发者再也不用在应用代码中维护一套网络路由规则了。所有的流量路由规则都被Envoy等服务代理所接管,由服务网格控制面提供动态的流量管控。
流量指挥官让服务网格和Envoy更容易在生产环境中使用。 —— Matt Klein, Envoy的创始人
流量指挥官是谷歌云全方位管控的服务网格流量控制面。流量指挥官对于虚拟机和容器都有效,它使用开源的xDS
接口来与数据面的服务代理进行交互。
你们很多人肯定都使用过谷歌面向互联网服务的全局负载均衡。流量指挥官把全局负载均衡带到了服务网格的微服务场景下。通过全局负载均衡,我们可以为你在GCP上全球部署的服务实例赋能。流量指挥官提供了一套智能的通信方案,是的客户端自动把流量发往容量有空余的最近服务节点。
这可以大大优化服务上下游之间的流量分布,为请求提供最短的RTT时间。
如果距离客户端最近的服务节点挂掉了,那么流量指挥官会智能无缝地把流量切换到最近的健康节点。
大规模部署的服务网格会产生大量的健康检查的流量,因为每一个代理都要去检查下游的所有服务的健康状态。当服务网格的体量增长的时候,健康检查就称为了一个 n2 的问题(假设 client 的数量和 server 的数量级都是n)。这会成为服务伸缩的一个重大障碍。
流量指挥官通过中心化的服务健康检查解决了这个问题。它使用一个全局分布式的弹性系统监控器去监控所有的服务实例。接下来,流量指挥官使用EDS API
,把健康检查的结果分发到全球所有的代理。
流量指挥官可以基于Load,支持自动扩容和缩容。Load信号是由proxy上报给它的。流量指挥官通知计算引擎扩/缩容到具体的大小。
当计算引擎扩容成功之前,流量指挥官会临时把流量打到其他可用的实例上,如果需要的话甚至会把流量临时打到其他的区域。一旦自动扩容成功,流量指挥官会把流量重新定位到最近的区域上。
流量指挥官是全面部署在GCP上的,你不用担心它的启动时间、生命周期管理、扩缩容、可用性等任何问题。流量指挥官的架构是全球分部署弹性部署的,它与Google自身的2C业务使用同样的系统。
流量指挥官可以提供 99.99% GA(Generally Available) 的 SLA(Service Level Aggreement)。
流量指挥官让你可以在不修改应用代码的情况下控制流量
你可以创建一个自定义的流量控制规则或者策略
使用上述路由规则和流量策略,你可以轻松地使得流量控制能力足够强大。
]]>如果不可能,则返回{-1, “”}。
我们保证 N >= 0
1 | N = 0 |
有的同学可能会误以为这是一个动态规划的题目,
可能是因为有一道题叫做“矩阵乘法”,这个题目相比来说很类似。
实际上因为它可以完全用贪心法,所以没有必要动态规划。
直接采用因式分解,然后把因子加起来就可以了。
1 |
|
21点游戏,英文:Blackjack,是使用扑克牌玩的赌博游戏。
A可作1点或11点,2-10作该牌之点数,J、Q、K作10点。
玩家初始手上有2张牌。
如果玩家要牌后,其手上拥有的牌的总点数超过21点,便要揭开手上所拥有的牌,称为爆牌。
反之若其手上拥有的牌的总点数不超过21点,该玩家可决定是否继续要牌。
假设一个玩家为了完成某项挑战,一定会选择要牌三次,那么对于一局游戏来说,他要牌三次仍未爆牌的概率是多少?
最近在玩R星开发的《荒野大镖客2》游戏,遇到了一个“赌徒系列挑战”之8:
当时耗费了2个小时不停地玩21点,终于完成了这个挑战。
这个挑战在网上被吐槽不少,只要在搜索引擎搜索“赌徒8”,全都是喷这个挑战的。大家都认为这个挑战的运气成分太大,而且完成这个挑战非常困难。
我突发奇想,想计算一下要牌3次还不爆牌的概率。(要牌3次就是有5张手牌)发现这是一个很好的概率题。
当然,对于真正的赌徒8挑战来说,想要算出完成赌徒8需要的期望局数,并不是3/不爆牌的概率。题目对于真正的游戏有一些简化。(尝试抽出最挑战大脑的部分,忽略其他细节,细节写在下面了。)
首先,不爆牌并不意味着能赢过庄家,庄家仍有可能比你的5张牌更接近21点;
其次,这个游戏没有办法在已经达到21点的时候继续要牌。譬如你起手牌为A
和K
,那么游戏会自动让你停牌,虽然这个时候你仍有可能摸5张牌而不爆,但是游戏机制决定你在这种情况下没办法继续摸牌了。
一共有52张牌。A,2,3,4,5,6,7,8,9,10,J,Q,K各四张。
因为题目要求是不爆牌,所以A看做11点是没有意义的,直接把A当做1点即可。
52张牌中任意选5张,一共有C(52,5)种可能,即2598960种。
剩下的只需要枚举出所有的不爆牌的情形即可。
使用模拟的方法,模拟1000000次情形,并统计不爆牌的次数,得出一个近似概率。
不是特别准确,但是也可以当做一个近似值。但是要真正较真的话,这是一个错误的解法。
实测:
1 | $ go run ./s01/main.go |
暴力枚举
通过迭代的方式,枚举出每一种方法,并计算出总的不爆牌的方法数目,最终除以总方法数,得到概率值。
该方法的核心点在于模拟。
算法复杂度
以总牌数为 M
, 需要摸的牌数为 N
,阈值为 T
。
时间复杂度
O(M^N)
指数级别,复杂度与阈值无关。
空间复杂度
很少,使用函数迭代,还有一个记录扑克的数组,勉强算是消耗 O(M+N)
的空间而已,
实测
1 | go run ./s02/main.go |
状态保留
我们调用numOfPossibleCases
函数的时候发现,很多参数一模一样的被调用了很多次。
我们建立一个HashMap保存一下状态,当遇到已经吊用过的函数时候,直接返回即可,不需要再次计算。
达到免除重复计算的剪枝效果。
算法复杂度
以总牌数为 M
, 需要摸的牌数为 N
,阈值为 T
。
时间复杂度
O(N*T*M^2)
将指数级别的复杂度降到了多项式级别。
空间复杂度
需要用一个 HashMap
来暂存状态
复杂度为O(N*T*M)
实测:
1 | go run ./s03/main.go |
动态规划
var dp [maxCardsToPick + 1][maxThreshold + 1][maxStart + 1]int
dp[i][j][k]的含义为:摸i张牌,最大不超过j,从第k张开始到最后一张牌是可选的范围,那么有几种摸牌的可能性。
状态转移方程:
1 | dp[i][j][k] = ∑ (l from k to totalNum) dp[i-1][j-pokers[l]][l+1] |
算法复杂度
以总牌数为 M
, 需要摸的牌数为 N
,阈值为 T
。
时间复杂度
O(N*T*M^2)
将指数级别的复杂度降到了多项式级别。
空间复杂度
建立了一个三维数组 var dp [maxCardsToPick + 1][maxThreshold + 1][maxStart + 1]int
复杂度为O(N*T*M)
1 | go run ./s04/main.go |
优化空间复杂度的动态规划
因为三维数组的每一层只和前面一层有关系,因此可以通过翻转的方式,减少内存消耗。
算法复杂度
以总牌数为 M
, 需要摸的牌数为 N
,阈值为 T
。
时间复杂度
和 Solution04 一致
空间复杂度
建立了一个三维数组 var dp [2][maxThreshold + 1][maxStart + 1]int
复杂度为O(T*M)
1 | go run ./s05/main.go |
代码库位置 https://github.com/pkumza/coding/tree/master/q01
附:测试环境
1 | MacBook Pro |
Pivotal 最早给出了相关的定义。
Cloud-native is an approach to building and running applications that exploits the advantages of the cloud computing delivery model. Cloud-native is about how applications are created and deployed, not where.
While today public cloud impacts the thinking about infrastructure investment for virtually every industry, a cloud-like delivery model isn’t exclusive to public environments. It’s appropriate for both public and private clouds. Most important is the ability to offer nearly limitless computing power, on-demand, along with modern data and application services for developers. When companies build and operate applications in a cloud-native fashion, they bring new ideas to market faster and respond sooner to customer demands.
Organizations require a platform for building and operating cloud-native applications and services that automates and integrates the concepts of DevOps, continuous delivery, microservices, and containers:
云原生是一种利用云计算交付模型的优势,来构建和运行应用程序的方法。
云原生的关注点在于应用程序如何创建和部署,而不关注在哪里部署。
虽然今天的公共云影响了几乎每个行业的基础设施投资思想,但类似云的交付模式并不仅限于公共环境。
它适用于公共云和私有云。
云原生应当始终是架构团队的追求目标。
当我们能够为业务团队按需提供充沛的计算能力,现代化的数据库和应用程序服务,那么业务团队也就能够更快地把新想法推向市场,并更快地响应客户请求。
组织需要一个平台来构建和运行云原生应用程序和服务,以自动化和集成DevOps,持续交付,微服务和容器的概念:
云原生计算基金会(Cloud Native Computing Foundation)给出了另一个定义。
Cloud native technologies empower organizations to build and run scalable applications in modern, dynamic environments such as public, private, and hybrid clouds. Containers, service meshes, microservices, immutable infrastructure, and declarative APIs exemplify this approach.
These techniques enable loosely coupled systems that are resilient, manageable, and observable. Combined with robust automation, they allow engineers to make high-impact changes frequently and predictably with minimal toil.
The Cloud Native Computing Foundation seeks to drive adoption of this paradigm by fostering and sustaining an ecosystem of open source, vendor-neutral projects. We democratize state-of-the-art patterns to make these innovations accessible for everyone.
云原生技术有利于各组织在公有云、私有云和混合云等新型动态环境中,构建和运行可弹性扩展的应用。
云原生的代表技术包括
容器
、服务网格
、微服务
、不可变基础设施
和声明式API
。
这些技术能够构建容错性好、易于管理和便于观察的松耦合系统。结合可靠的自动化手段,云原生技术使工程师能够轻松地对系统作出频繁和可预测的重大变更。
云原生计算基金会(CNCF)致力于培育和维护一个厂商中立的开源生态系统,来推广云原生技术。我们通过将最前沿的模式民主化,让这些创新为大众所用。
系统提供了wall time 和 monotonic 时间。
其中wall time会根据时间校准而改变,而monotonic不会。
举个例子。
你写了一个日志收集程序,每10秒把收到的日志汇报给流式收集系统。
但是你的机器事件比真实时间快了1分钟。
1 | now := time.Now() |
假如得到now的时间以后,机器与外部时间进行了一步同步,把本机实际时间调慢1分钟,那么程序将会在第二行卡上70秒。
这会导致本地日志队列可能会爆仓。
与Duration相关的时间操作,譬如time.Since(start), time.Until(deadline), and time.Now().Before(deadline)等,对于时间重置非常敏感,所以它们需要参考 Monotonic 时间。
而如果时间是为了打印,或者start time/deadline time是外界传入的,那么 monotonic 时间将会是一个干扰。 此时应该使用 t = t.Round(0) 排除 monotonic。
]]>from https://github.com/envoyproxy/envoy/blob/stable/v1.7.1/api/envoy/api/v2/cds.proto#L119
1 | enum LbPolicy { |
ROUND_ROBIN是使用最为广泛的LB算法,轮询。
举个例子:
1,2,3,4,5,6,六个请求依次访问有两个后端的LB,那么依ROUND_ROBIN算法,
服务器A服务1,3,5
服务器B服务2,4,6
Envoy的ROUND_ROBIN实际上是加权的。
由于服务后端的处理能力不同。假如服务器A能力比较强,我们分配了Weight=2;B的服务能力一般,我们分配了Weight=1,那么依照Weighted Round-Robin
服务器A服务1,2,4,5
服务器B服务3,6
LEAST_REQUEST的意思是把请求送给当前活跃请求最少的服务器。
因为不同的请求需要耗费的资源实际上也是不一样的,我们假设2,4,6请求都特别耗费资源,1,3,5请求都很简单,那么把2,4,6都分配给服务器B就不甚合理。
1,2两个请求来了
服务器A服务1
服务器B服务2
1很快结束了,2还卡单
请求3来了,由于B现在有1个active req,A没有,所以分配给A
请求3也很快处理完
请求4来了,由于B现在有1个active req,A没有,所以分配给A
两个都在卡单。
请求5和6来了,分别分配给A,B。
最终,所有请求解决。
综上
服务器A服务1,3,4,5
服务器B服务2,6
环形哈希是最基本的一致性哈希算法
需要设置hash key
来了一个新的请求,需要得到下游的时候,需要对Lookup Table进行二分查找。
当Lookup Table的大小为N的时候,需要Log(N)的查找时间复杂度。
N太大,每次查找时间太长;
N太小,环形哈希的均匀性又不够。
这个地方需要Trade-off,或者可以选用Maglev算法。
纯随机
一般效果还不错。
原始目的地负载均衡
upstream的主机基于downstream的连接元数据。这个是envoy特定的,不详细讲了。
一种特殊的一致性哈希算法,效率比环形哈希要高一些,增删节点的影响一般更小。
需要设置hash key
首先对每一个后端节点产生一个permutation;
然后用一种特殊的walk算法,得到一个Lookup Table。
新的请求到来的时候,只需要直接查表即可,时间复杂度为O(1)。
walk算法保证哈希的概率是基本均匀的。
Envoy中有一个监控恐慌阈值的设置,这个设置很有意思,所以在这里额外讲一下。
负载均衡一般是根据集群中主机的健康情况灵活变动的。当某台主机跪了,LB算法将会把它从候选列表中踢出去,这也是很合理的。
但是我们假设这么一种情况,某一时间,所有服务主机的负载情况是最大负载的80%,(负载800;最大处理能力1000)
因为某种原因,导致20.0%的机器彻底崩溃。(负载800;最大处理能力800)
LB策略忽略20%的机器,导致剩下的80%的机器都在最大处理负载上运行;
又来了一个网络波动,造成所有的服务器一个接一个崩溃,整个集群雪崩。
每拉起一台新的机器,LB策略立刻把所有的流量打到这么一台机器上,导致它再次崩溃。
如果有一个恐慌阈值,譬如50%,那么LB会在50%机器崩溃的时候,禁用淘汰策略,把所有机器都当做健康的,在整体集群上执行普通的Round-Robin策略。
多数机器恢复,整个集群的处理能力恢复80%的正确率。这使得整个集群能够在遇到极特殊情况的时候能够从困境中恢复。
通用运营后台
火山和抖音都有搜索彩蛋的需求 就是搜索某个特定词的时候 出一些彩蛋,配合广告售卖或者活动
大家接到需求的想法就是 写个运营后台,数据存入mysql,然后再起个脚本,定时把数据按照格式dump到redis,在线服务读redis判断
或者说 单独抽象一个服务,运营后台负责写入,在线业务负责读取
搜索结果过滤条件
客户端配置,广告、图片链接配置,客户端Toast提示文本内容。
你可以设置用户满足什么地域、UID在哪个灰度的情况下,更换某一项内容。
初代不成熟协议,只支持GET
命令,仅接受HTML格式的子串,一次请求结束之后TCP连接随即关闭。
不仅支持HTML,还支持图片、视频、二进制文件等。
增加了 POST HEAD,
每次Req和Resp都必须含有头部。整个头部只能使用ASCII编码
新增了状态码、权限、缓存、内容编码(content encoding)等功能。
新增Content-Type字段。
缺点:
但是每次只能发送一个请求,三次握手比较慢,另外需要慢启动。
连接复用采用 Connection: keep-alive
字段,该字段非标准。
RFC 2616
第一个有正式标准的版本。
举个 🌰
1 | $> telnet website.org 80 |
①:附带了encoding,charset和cookie元信息请求HTML文件
②:分块传输结果
③:分块的大小,以ASCII 十六进制表示。 100代表256字节。
④:用0,代表整个Response分块结束。
⑤:使用同一条TCP连接请求图标文件
⑥:提醒服务器,这个连接将不再被复用,可以关闭连接。
⑦:返回图标文件的信息,并在结尾处关闭连接。
以上样例重点贯彻了HTTP/1.1
的连接复用、分块以及显式关闭等特征。
特征:
其他功能
增加了 content, encoding, character set 等字段。
Host 字段
PUT PATCH HEAD OPTIONS DELETE
缺点
队头堵塞 Head-of-line blocking
谷歌自研 2009年 ,HTTP/2的基础
注意,不叫HTTP/2.0
,下一个版本将是HTTP/3
。
二进制协议
HTTP/1.1 的header肯定是ASCII,数据体可以是文本或二进制。
HTTP/2 是一个彻底的二进制协议,header和body都是二进制,并且统称为帧
:头信息帧和数据帧。
多工 Multiplexing
HTTP/2 复用了TCP连接,在一个连接里,客户端和浏览器都可以同时发送多个请求或回应,而不是按照顺序。这避免了队头堵塞
。
数据流
因为HTTP/2的数据包不按顺序发送,所以要指出每个数据包属于哪个回应。
每个请求或回应的所有数据包,称为一个数据流。每个数据流有个独一无二的ID。
客户端来的数据包的ID是奇数,服务端发送的数据包ID是偶数。
头信息压缩
一方面头可以压缩;另一方面,客户端和服务端维护了一张索引表,不用发送相同的字段。
服务器推送
未经过客户端的主动请求,服务器可以主动向客户端发送资源。
参考:
http://www.ruanyifeng.com/blog/2016/08/http.html
https://github.com/abbshr/rfc7540-translation-zh_cn
https://tools.ietf.org/html/rfc7540 / https://httpwg.org/specs/rfc7540.html
https://developers.google.com/web/fundamentals/performance/http2/?hl=zh-cn
https://hpbn.co/brief-history-of-http/
在我们具体地讨论屏幕快照之前,我们先讲一下基本使用(经验丰富的用户可以跳过这个部分)
在Mac OS X中,有三种方式来截图:截取整个屏幕、截取选定的窗口、截取指定方形区域,以上每一个都可以用快捷键唤起。
Command + Shift + 3: 截取整个屏幕。如果你有多个屏幕,那么每个屏幕会分别被截在不同的图中。
Command + Shift + 4: 截取一个方形区域。接下来你可以用你的鼠标划取这个区域。
Command + Shift + 4, 然后按下空格: 截取一个窗口区域。
使用上述快捷键,默认会把截图保存到桌面上。
如果你在按上述快捷键的时候,同时按住Control键,那么截到的图片会暂时存在剪贴板中。
除了上述三个功能之外,/应用程序/实用工具/抓图.app还提供定时截图的功能。它可以有一个10秒钟的等待时间,会在你点击启动定时器
按钮之后的10秒钟完成截图。
后续每个效果,都得通过执行这句指令才能生效。
1 | killall SystemUIServer |
这句话起到了刷新作用。
1 | defaults write com.apple.screencapture type [format] |
[format]
可以输入为
譬如,你可以通过输入defaults write com.apple.screencapture type jpg
把默认图片格式改为jpg
系统默认存储的文件名为屏幕快照 [date] [time].[format]
,譬如屏幕快照 2018-07-31 下午4.01.16.png
。
你无法把时间戳从文件名中移除,但是你可以改掉屏幕快照
这个前缀。方法是输入:
1 | defaults write com.apple.screencapture name [file name] |
默认是保存到桌面的。你可以手动改变保存位置。
命令是:
1 | defaults write com.apple.screencapture location |
首先,你必须要创建一个新的文件夹,当然你要了解你文件夹的位置。
譬如你创建了一个名字叫做abc
的文件夹在桌面上,那么这个文件夹的位置就是~/Desktop/abc
例:
1 | defaults write com.apple.screencapture location /Users/[username]/Desktop/abc/ |
截图的时候默认是有窗口阴影的,你可以用命令改掉。
1 | defaults write com.apple.screencapture disable-shadow -bool true |
效果图: