2024-08-24 21:13:28
摘要:一、缓存
基本上来说,在分布式系统中最耗性能的地方就是最后端的数据库了。
一般来说,只要小心维护好,数据库四种操作(select、update、insert 和 delete)中的三个写操作 insert、update 和 delete 不太会出现性能问题(insert 一般不会有性能问题,update 和 delete 一般会有主键,所以也不会太慢)。除非索引建得太多,而数据库里的数据又太多,这三个操作才会变慢。
绝大多数情况下,select 是出现性能问题最大的地方。一方面,select 会有很多像 join、group、order、like 等这样丰富的语义,而这些语义是非常耗性能的;另一方面,大多数应用都是读多写少,所以加剧了慢查询的问题。
分布式系统中远程调用也会消耗很多资源,因为网络开销会导致整体的响应时间下降。为了挽救这样的性能开销,在业务允许的情况下,使用缓存是非常必要的事情。
缓存是提高性能最好的方式,一般来说,缓存有以下三种模式。
1. Cache Aside 更新模式
这是最常用的设计模式了,其具体逻辑如下。
失效:应用程序先从 Cache 取数据,如果没有得到,则从数据库中取数据,成功后,放到缓存中。
命中:应用程序从 Cache 中取数据,取到后返回。
更新:先把数据存到数据库中,成功后,再让缓存失效。
这是标准的设计模式,为什么不是写完数据库后更新缓存?主要是怕两个并发的写操作导致脏数据。
那么,是不是这个 Cache Aside 就不会有并发问题了?不是的。比如,一个是读操作,但是没有命中缓存,就会到数据库中取数据。而此时来了一个写操作,写完数据库后,让缓存失效,然后之前的那个读操作再把老的数据放进去,所以会造成脏数据。
这个案例理论上会出现,但实际上出现的概率可能非常低,因为这个条件需要发生在读缓存时缓存失效,而且有一个并发的写操作。实际上数据库的写操作会比读操作慢得多,而且还要锁表,读操作必须在写操作前进入数据库操作,又要晚于写操作更新缓存,所有这些条件都具备的概率并不大。
当然,最好还是为缓存设置好过期时间。
2. Read/Write Through 更新模式
在 Cache Aside 套路中,应用代码需要维护两个数据存储,一个是缓存,一个是数据库。所以,应用程序比较啰嗦。而 Read/Write Through 套路……
阅读全文
2024-08-17 18:04:48
摘要:一、分布式锁
我们知道,在多线程情况下访问一些共享资源需要加锁,不然就会出现数据被写乱的问题。在分布式系统下,这样的问题也是一样的。只不过,我们需要一个分布式的锁服务。
分布式的锁服务需要有以下几个特点。
安全性(Safety):在任意时刻,只有一个客户端可以获得锁(排他性)。
避免死锁:客户端最终一定可以获得锁,即使锁住某个资源的客户端在释放锁之前崩溃或者网络不可达。
容错性:只要锁服务集群中的大部分节点存活,Client 就可以进行加锁解锁操作。
1. Redis 的分布式锁服务
我们通过以下命令对资源加锁。
SET resource_name my_random_value NX PX 30000
解释一下:
SET NX 命令只会在 key 不存在的时候给 key 赋值,PX 命令通知 Redis 保存这个 key 30000ms。
my_random_value 必须是全局唯一的值。这个随机数在释放锁时保证释放锁操作的安全性。
PX 操作后面的参数代表的是这个 key 的存活时间,称作锁过期时间。
当资源被锁定超过这个时间时,锁将自动释放。
获得锁的客户端如果没有在这个时间窗口内完成操作,就可能会有其他客户端获得锁,引起争用问题。
通过下面的脚本为申请成功的锁解锁:
if redis.call(get,KEYS[1]) == ARGV[1] then
return redis.call(del,KEYS[1])
else
return 0
end
如果 key 对应的 value 一致,则删除这个 key。通过这个方式释放锁是为了避免 Client 释放了其他 Client 申请的锁。
2. 分布式锁服务的一个问题
虽然 Redis 文档里说他们的分布式锁是没有问题的,但其实还是很有问题的。尤其是上面那个为了避免 Client 端把锁占住不释放,然后,Redis 在超时后把其释放掉,这事儿听起来就有点不靠谱。
我们来脑补一下,不难发现下面这个案例。
1. 如果 Client A 先取得了锁。
2. Client B 在等待 Client A 的工作完成。
3. 这个时候,如果 Client A 被挂在了某些事上,比如一个外部的阻塞调用,或是 CPU 被别的进程吃满,或是不巧碰上了 Full GC,导致 Client ……
阅读全文
2024-08-10 21:04:04
摘要:对于分布式系统的容错设计,在英文中又叫 Resiliency(弹力)。意思是,系统在不健康、不顺,甚至出错的情况下有能力 hold 得住,挺得住,还有能在这种逆境下力挽狂澜的能力。其中着眼于分布式系统的各种“容忍”能力,包括服务隔离、异步调用、请求幂等性、可伸缩性(有 / 无状态的服务)、一致性(补偿事务、重试)、应对大流量的能力(熔断、降级)。可以看到,在确保系统正确性的前提下,系统的可用性是弹力设计保障的重点。
我们很难计算我们设计的系统有多少的可用性,因为影响一个系统的因素实在是太多了,除了软件设计,还有硬件,还有第三方服务(如电信联通的宽带 SLA),当然包括“建筑施工队的挖掘机”,宕机原因主要有以下这些:
网络问题。网络链接出现问题,网络带宽出现拥塞……
性能问题。数据库慢 SQL、Java Full GC、硬盘 IO 过大、CPU 飙高、内存不足……
安全问题。被网络攻击,如 DDoS 等。
运维问题。系统总是在被更新和修改,架构也在不断地被调整,监控问题……
管理问题。没有梳理出关键服务以及服务的依赖关系,运行信息没有和控制系统同步……
硬件问题。硬盘损坏、网卡出问题、交换机出问题、机房掉电、挖掘机问题……
一个分布式系统的故障是非常复杂的,因为故障是分布式的、多米诺骨牌式的。所以,要充分地意识到下面两个事。
故障是正常的,而且是常见的。
故障是不可预测突发的,而且相当难缠。
这就是为什么我们把这个设计叫做弹力(Resiliency)。
一方面,在好的情况下,这个事对于我们的用户和内部运维来说是完全透明的,系统自动修复不需要人的干预。
另一方面,如果修复不了,系统能够做自我保护,而不让事态变糟糕。
一、隔离设计
隔离设计对应的单词是 Bulkheads,中文翻译为隔板,这个概念来自于船舱里防漏水的隔板。我们的软件设计当然也“漏水”,所以为了不让“故障”蔓延开来,需要使用“隔板”技术,来将架构分隔成多个“船舱”来隔离故障。
1. 按服务的种类来做分离
上图中,我们将系统分成了用户、商品、社区三个板块。这三个块分别使用不同的域名、服务器和数据库,做到从接入层到应用层再到数据层三层完全隔离。这样一来,在物理上来说,一个板块的故障就不会影响到另一板块。
上面这种架构虽然在系统隔离上做得比较好,但是也存在以下一些问题。
如果我们需要同时获得多……
阅读全文
2024-08-03 11:55:30
摘要:我们一直在谈论各式各样的架构,如高并发架构、异地多活架构、容器化架构、微服务架构、高可用架构、弹性化架构等。还有和这些架构相关的管理型的技术方法,如 DevOps、应用监控、自动化运维、SOA 服务治理、去 IOE 等。面对这么多纷乱的技术,很多团队或是公司都是一个一个地去做这些技术,非常辛苦,也非常累。
接下来我们来谈一谈分布式架构。
一、概述
1. 分布式的优缺点
首先,为什么需要分布式系统,而不是传统的单体架构。
增大系统容量。我们的业务量越来越大,而要能应对越来越大的业务量,一台机器的性能已经无法满足了,我们需要多台机器才能应对大规模的应用场景。所以,我们需要垂直或是水平拆分业务系统,让其变成一个分布式的架构。
加强系统可用。我们的业务越来越关键,需要提高整个系统架构的可用性,这就意味着架构中不能存在单点故障。这样,整个系统不会因为一台机器出故障而导致整体不可用。所以,需要通过分布式架构来冗余系统以消除单点故障,从而提高系统的可用性。
当然,分布式系统还有一些优势,比如:
因为模块化,所以系统模块重用度更高;
因为软件服务模块被拆分,开发和发布速度可以并行而变得更快;
系统扩展性更高;团队协作流程也会得到改善;
……
不过,这个世界上不存在完美的技术方案,采用任何技术方案都是“按下葫芦浮起瓢”,都是有得有失,都是一种 trade-off。也就是说,分布式系统在解决上述问题的同时,也给我们带来了其他的问题。因此,我们需要清楚地知道分布式系统所带来的问题。
从上面的表格我们可以看到,分布式系统虽然有一些优势,但也存在一些问题。
架构设计变得复杂(尤其是其中的分布式事务)。
部署单个服务会比较快,但是如果一次部署需要多个服务,流程会变得复杂。
系统的吞吐量会变大,但是响应时间会变长。
运维复杂度会因为服务变多而变得很复杂。
架构复杂导致学习曲线变大。
测试和查错的复杂度增大。
技术多元化,这会带来维护和运维的复杂度。
管理分布式系统中的服务和调度变得困难和复杂。
2. 面向服务的架构有以下三个阶段
下面是一个 SOA 架构的演化图。
我们可以看到,面向服务的架构有以下三个阶段。
20 世纪 90 年代前,是单体架构,软件模块高度耦合。当然,这张图同样也说明了有的 SOA 架构其实和单体架构没什么两样,因为都是高度耦合在一起的。就像图中的齿轮一……
阅读全文
2021-12-11 16:58:41
摘要:一、Kafka 基础概念
1. kafka 简介
Kafka 是由 LinkedIn 开发并开源的分布式消息系统,因其分布式及高吞吐率而被广泛使用,现已与Cloudera Hadoop,Apache Storm,Apache Spark集成。
Kafka 已被多家不同类型的公司作为多种类型的数据管道和消息系统使用。行为流数据是几乎所有站点在对其网站使用情况做报表时都要用到的数据中最常规的部分。
包括页面访问量 PV、页面曝光 Expose、页面点击 Click 等行为事件;
实时计算中的 Kafka Source,Dataflow Pipeline;
业务的消息系统,通过发布订阅消息解耦多组微服务,消除峰值;
Kafka 是一种分布式的,基于发布/订阅的消息系统。主要设计目标如下:
以时间复杂度为 O(1) 的方式提供消息持久化能力,即使对 TB 级以上数据也能保证常数时间复杂度的访问性能;
高吞吐率。即使在非常廉价的商用机器上也能做到单机支持每秒 100K 条以上消息的传输;
支持 Kafka Server 间的消息分区,及分布式消费,同时保证每个 Partition 内的消息顺序传输;
同时支持离线数据处理和实时数据处理;
Scale out:支持在线水平扩展;
2. 为何使用消息系统
解耦:消息系统在处理过程中间插入了一个隐含的、基于数据的接口层,两边的处理过程都要实现这一接口。这允许你独立的扩展或修改两边的处理过程,只要确保它们遵守同样的接口约束。
而基于消息发布订阅的机制,可以联动多个业务下游子系统,能够不侵入的情况下分步编排和开发,来保证数据一致性。
冗余:有些情况下,处理数据的过程会失败。除非数据被持久化,否则将造成丢失。消息队列把数据进行持久化直到它们已经被完全处理,通过这一方式规避了数据丢失风险。许多消息队列所采用的”插入-获取-删除”范式中,在把一个消息从队列中删除之前,需要你的处理系统明确的指出该消息已经被处理完毕,从而确保你的数据被安全的保存直到你使用完毕。
扩展性:因为消息队列解耦了你的处理过程,所以增大消息入队和处理的频率是很容易的,只要另外增加处理过程即可。不需要改变代码、不需要调节参数。扩展就像调大电力按钮一样简单。
灵活性 峰值处理能力:在访问量剧增的情况下,应用仍然需要继续发挥作用,但是这样的突发流量并不常见……
阅读全文
2021-12-04 13:44:12
摘要:一、DNS
1. DNS 介绍
DNS(Domain Name System,域名系统),DNS 服务用于在网络请求时,将域名转为 IP 地址。能够使用户更方便的访问互联网,而不用去记住能够被机器直接读取的 IP 数串。
传统的基于 UDP 协议的公共 DNS 服务极易发生 DNS 劫持,从而造成安全问题。
递归查询:如果主机所询问的本地域名服务器不知道被查询域名的 IP 地址,那么本地域名服务器就以 DNS 客户的身份,向其他根域名服务器继续发出查询请求报文,而不是让该主机自己进行下一步的查询。
迭代查询:当根域名服务器收到本地域名服务器发出的迭代查询请求报文时,要么给出所要查询的 IP 地址,要么告诉本地域名服务器:你下一步应当向哪一个域名服务器进行查询。然后让本地域名服务器进行后续的查询,而不是替本地域名服务器进行后续的查询。
由此可见,客户端到 Local DNS 服务器,Local DNS 与上级 DNS 服务器之间属于递归查询;DNS 服务器与根 DNS 服务器之前属于迭代查询。
DNS 可以做三层 LBS。
2. DNS 的问题
Local DNS 劫持:Local DNS 把域名劫持到其他域名,实现其不可告人的目的。
域名缓存就是 LocalDNS 缓存了业务的域名的解析结果,不向权威 DNS 发起递归。
保证用户访问流量在本网内消化:国内的各互联网接入运营商的带宽资源、网间结算费用、IDC机房分布、网内 ICP 资源分布等存在较大差异。为了保证网内用户的访问质量,同时减少跨网结算,运营商在网内搭建了内容缓存服务器,通过把域名强行指向内容缓存服务器的 IP 地址,就实现了把本地本网流量完全留在了本地的目的。
推送广告:有部分 LocalDNS 会把部分域名解析结果的所指向的内容缓存,并替换成第三方广告联盟的广告。
除了域名缓存以外,运营商的 LocalDNS 还存在解析转发的现象。
解析转发是指运营商自身不进行域名递归解析,而是把域名解析请求转发到其它运营商的递归 DNS 上的行为。
而部分小运营商为了节省资源,就直接将解析请求转发到了其它运营的递归 LocalDNS 上去了。
这样的直接后果就是权威 DNS 收到的域名解析请求的来源 IP 就成了其它运营商的 IP,最终导致用户流量被导向了错误的 IDC,……
阅读全文
2021-11-27 21:04:45
摘要:一、弹幕概述
1. 弹幕的几个概念
弹幕模式 mode:
从右向左(从左到右)滚动的弹幕
底部(顶部)弹幕
代码弹幕
脚本弹幕
弹幕池 pool:
普通弹幕池
字幕弹幕
特殊弹幕
推荐弹幕
被up主屏蔽的弹幕
note: 普通弹幕池弹幕长度限制300,特殊弹幕池无长度限制
弹幕上限 maxlimit:
每个视频要展示的弹幕数,根据视频时长设置,普通弹幕 + 保护弹幕,以及推荐弹幕最大展示 maxlimit 条,字幕弹幕、特殊弹幕不受限
常规值:100,300,500,1500,3000,6000,8000,黑科技值:16000,32000
2. 弹幕的几个特点
实时性要求不高:
弹幕文件不像其他业务需要实时性的加载
实时性弹幕可以通过弹幕广播实现
一次性加载:
和评论业务不同,弹幕需要在视频点击播放后一次性加载,comment.bilibili.com/cid.xml
弹幕文件比较大
用户相对敏感,有些用户比较关注自己发过的弹幕是否存在
3. 弹幕的现状
数据总量:100亿+
单表记录:7000万+,且按照 cid 范围分表,导致数据分布严重不均
单个视频弹幕数:500w+
单个视频展示条数:极个别 1w+,常规 3000~8000,且一次性返回
单条弹幕大小:长度无上限,可以通过文本上传
单个弹幕文件超过 10M http://comment.bilibili.com/4279031.xml
数据库慢查询多如狗
二、老弹幕系统架构
1. 架构图
2. 老系统刷新逻辑
dmCachecheck 通过扫描 dm_index 中的标志位来找到所有视频分批弹幕数
通过databus发送稿件弹幕数到 Stat-T
3. 老系统弹幕缓存
何时刷新缓存
dm_needupdate: memcache缓存刷新标志
dm_cacheupdate: memcache+redis缓存刷新标志
由于弹幕 xml 超过 1M,缓存如何存放,由于 memcache 对于大于1M文件的存取存在一些问题,因此生成缓存后将大于1M的 xml,放入NFS文件系统
优先从哪里获取缓存,老的弹幕系统在弹幕的索引 dm_index 表中增加 childpool 字段标识应该优先从哪里获取缓存,childpool=2:文件系统……
阅读全文
2021-11-20 22:30:40
摘要:一、网络通信协议
互联网的核心是一系列协议,总称为”互联网协议”(Internet Protocol Suite),正是这一些协议规定了电脑如何连接和组网。
主要协议分为:
Socket:接口抽象层
TCP / UDP:面向连接(可靠) / 无连接(不可靠)
HTTP1.1 / HTTP2 / QUIC(HTTP3):超文本传输协议
1. Socket 抽象层
应用程序通常通过“套接字”向网络发出请求或者应答网络请求。
一种通用的面向流的网络接口,主要操作:
建立、接受连接
读写、关闭、超时
获取地址、端口
2. TCP 面向连接的协议
TCP/IP(Transmission Control Protocol/Internet Protocol)即传输控制协议/网间协议,是一种面向连接(连接导向)的、可靠的、基于字节流的传输层(Transport layer)通信协议,因为是面向连接的协议。
服务端流程:
监听端口
接收客户端请求建立连接
创建 goroutine 处理连接
客户端流程:
建立与服务端的连接
进行数据收发
关闭连接
3. UDP 不可靠连接
UDP 协议(User Datagram Protocol)中文名称是用户数据报协议,是 OSI(Open System Interconnection,开放式系统互联)参考模型中一种无连接的传输层协议。
一个简单的传输层协议:
不需要建立连接
不可靠的、没有时序的通信
数据报是有长度(65535-20=65515)
支持多播和广播
低延迟,实时性比较好
应用于用于视频直播、游戏同步
4. HTTP 超文本传输协议
HTTP(HyperText Transfer Protocol)是互联网上应用最为广泛的一种网络协议,它详细规定了浏览器和万维网服务器之间互相通信的规则,通过因特网传送万维网文档的数据传送协议。
请求报文:
Method: HEAD/GET/POST/PUT/DELETE
Accept:text/html、application/json
Content-Type:
application/json
application/x-www-form-urlencoded
请求正文
响应报文:
状态行(200/400/500)
响应头(Response Head……
阅读全文
2021-09-11 20:14:00
摘要:一、日志
1. 日志级别
Warning:
没人看警告,因为从定义上讲,没有什么出错。也许将来会出问题,但这听起来像是别人的问题。
我们应该尽可能的消除警告级别,它要么是一条信息性消息,要么是一个错误。
参考 Go 语言设计额哲学,所有警告都是错误,其他语言的 warning 都可以忽略,除非 IDE 或者在 CICD 流程中强制他们为 error,然后逼着程序员们尽可能去消除。
同样的,如果想要最终消除 warning 可以记录为 error,让代码作者重视起来。
Fatal,记录消息后,直接调用 os.Exit(1),这意味着:
在其他 goroutine defer 语句不会被执行;
各种 buffers 不会被 flush,包括日志的;
临时文件或者目录不会被移除;
不要使用 fatal 记录日志,而是向调用者返回错误。如果错误一直持续到 main.main。main.main 那就是在退出之前做处理任何清理操作的正确位置。
Error,也有很多人,在错误发生的地方要立马记录日志,尤其要使用 error 级别记录。
处理 error;
把 error 抛给调用者,在顶部打印日志;
如果您选择通过日志记录来处理错误,那么根据定义,它不再是一个错误——您已经处理了它。记录错误的行为会处理错误,因此不再适合将其记录为错误。
err := somethingHard()
if err != nil {
log.Error(oops, something was too hard, err)
return err // what is this, Java ?
}
if err := planA(); err != nil {
log.Infof(could't open the foo file, err)
planB()
}
这里产生了降级行为,本质属于有损服务,更倾向在这里使用 Warning。
Debug,相信只有两件事你应该记录:
开发人员在开发或调试软件时关心的事情。
用户在使用软件时关心的事情。
显然,它们分别是调试和信息级别。
log.Info 只需将该行写入日志输出。不应该有关闭它的选项,因为用户只应该被告知对他们有用的事情。如果发生了一个无法处理的错误,它就会抛出到 main.ma……
阅读全文
2021-09-04 10:57:12
摘要:一、缓存选型
1. Memcache
memcache 提供简单的 kv cache 存储,value 大小不超过1mb。
memcache 用来存储大文本或者简单的 kv 结构。
memcache 使用了slab 方式做内存管理,存在一定的浪费,如果大量接近的 item,建议调整 memcache 参数来优化每一个 slab 增长的 ratio、可以通过设置 slab_automove slab_reassign 开启memcache 的动态/手动 move slab,防止某些 slab 热点导致内存足够的情况下引发 LRU。
大部分情况下,简单 KV 推荐使用 Memcache,吞吐和相应都足够好。
内存分配方式
每个 slab 包含若干大小为1M的内存页,这些内存又被分割成多个 chunk,每个 chunk 存储一个 item;
在 memcache 启动初始化时,每个 slab 都预分配一个 1M 的内存页,由slabs_preallocate 完成(也可将相应代码注释掉关闭预分配功能)。
chunk 的增长因子由 -f 指定,默认1.25,起始大小为48字节。
内存池有很多种设计,可以参考下: nginx ngx_pool_t,tcmalloc 的设计等等。
2. Redis
redis 有丰富的数据类型,支持增量方式的修改部分数据,比如排行榜,集合,数组等。
比较常用的方式是使用 redis 作为数据索引,比如评论的列表 ID,播放历史的列表 ID 集合,关系链列表 ID。
redis 因为没有使用内存池,所以是存在一定的内存碎片,老版本一般会使用 jemalloc 来优化内存分配,需要编译时候使用 jemalloc 库代替 glib 的 malloc 使用。
3. Redis vs Memcache
Redis 和 Memcache 最大的区别其实是 redis 单线程(新版本双线程),memcache 多线程,所以 QPS 可能两者差异不大,但是吞吐会有很大的差别,比如大数据 value 返回的时候,redis qps 会抖动下降的的很厉害,因为单线程工作,其他查询进不来(新版本有不少的改善)。
所以建议纯 kv 都走 memcache,比如B站的关系链服务中用了 hashs 存储双向关系,但是也会使用 memcache 档一层来避免 hge……
阅读全文