Alvin Liu

  • Home
  • About
  • Privacy Policy
  1. Main page
  2. High Performance
  3. Main content

亿级流量网站架构读书笔记

2023-02-27 1922hotness 1likes 0comments

感谢张开涛先生为我们分享了互联网高并发场景的经典问题和解决方案。阅读后深有感触,故整理读书笔记于此。感兴趣的小伙伴请在这里购买 https://item.jd.com/12153914.html

系统设计基本方法

高并发

  1. 无状态
    1. 机房状态可配置
    2. 服务状态存数据库
  2. 服务化
    1. 按业务线部门进行拆分
    2. 读写维度
  3. 异步
    1. 发布 - 订阅
    2. 对热点队列进行镜像复制
    3. 异步就是牺牲一致性
      1. 定期扫描校对
  4. 缓存
    1. 缓存无处不在
      1. 浏览器/客户端
      2. 本地网络代理服务器
      3. CDN/镜像服务器/P2P
      4. Nginx, Redis, 静态化, Guava, CPU, http内容缓存Varnish或者Squid
    2. 缓存优化
      1. 过期时间, 推送, 预加载
      2. CDN链接不能有随机数, 否则回源到站点服务器
      3. 会频繁变化的数据不要缓存, 否则超时才会刷新
      4. nginx进行一致性哈希, 使同一商品的请求落到同一台服务器, 提高缓存命中率. 类似session sticky.
      5. 内容服务的缓存效果较好, 信息查询不好缓存
    3. 服务端缓存分层
      1. 接入层varnish, Squid专业http缓存. 或者Nginx+Lua读取lua_shared_dict或集群Redis
      2. 应用层Tomcat读写本地Redis或集群Redis
      3. JVM读取Caffeine/Guava等内存缓存
  5. 数据冗余

高可用

服务降级

  1. 从缓存读取
  2. 接入层返回, Nginx
  3. 异步写, 暂时写到Redis, 消息队列或文件日志就返回. 之后由Worker异步写DB.

限流

  • 从缓存读取, 比如检测到爬虫
  • Nginx conn_limit module
  • Nginx deny IP
  • 要保护好应用层

切流量

  • DNS切机房
  • 客户端直接通过IP访问, 绕过本地DNS, IP从服务器推送
  • LVS (机房LB) 切应用集群(Nginx)
  • Nginx(Ingress)切应用服务器(Pod)

业务设计

  • 防止重复更新, 高并发异步操作很容易多次调用
    1. 幂等设计: 计算生成数据ID, 确保同一个请求的ID总是一样的.
    2. 防重表: Worker定期扫描去重
  • 状态机: 使用状态机来管理业务数据的状态转换
    1. 比如下单流程: 订单, 支付, 待发货, 已发货, 已完成
    2. 还有状态的分支或回滚: 订单, 支付, 待发货, 待取消, 退款, 已取消
  • 审批/审计: 敏感操作要有人员审批流程. 修改操作要有审计日志
  • 文档/注释: 代码的文档和日志可以辅助修复问题
  • 系统监控: 日志, 服务器, 机房数据采集, 展示, 告警策略, 告警渠道, 值班人员.
  • 应急预案: 故障分级和上报流程, 各级负责人表, 值班计划表, hotfix/datafix部署流程, 故障总结表

高可用

负载均衡与反向代理

负载均衡分层

负载均衡可以应用在OSI(Open System Interconnection)网络模型的不同层.

层级层级名实现方案产品
二层链路层修改MAC地址LVS DR
四层传输层修改IP地址和端口LVS NATHaProxyNginx
七层应用层URL转发到IP:端口Nginx

越底层性能越好, 因为不用拆包

越高层功能越强, 因为有更多信息可以设置转发条件

负载均衡算法

  • 轮询
  • IP hash (sticky), 再散列(一致性哈希, 解决增删节点问题)
  • 最少连接数, 压力感知

长连接

Nginx可配置和后端(上游upstream)服务器保持长连接

动态配置上游服务器

Nginx集成服务发现(Consul + etcd)自动添加新的上游服务器

对应到K8s就是Ingress

OpenResty

OpenResty是Nginx的一个module, 主要功能是运行Lua脚本

所有Nginx的Lua脚本都需要使用OpenResty module执行.

OpenResty本身也是web服务器, 可以响应http请求

服务隔离

线程隔离

  1. Tomcat Servlet线程池分级, 把请求转到应用自己的线程池运行
    1. 应用可以根据请求优先级建立多个线程池, 合理分配资源
    2. 单一线程池故障不影响其他线程池的请求
  2. 应用自建线程池方案
    1. 使用Servlet 3异步响应机制AsyncContext, AsyncResponse
    2. 使用Hystrix发送请求都会在Hystrix的线程池来运行

读写隔离

各机房都有只读存储, 提高性能还可以备份

静态资源隔离CDN

静态资源不论大小都要上CDN.

js/css发布时应该带有版本号, 这样出问题不需要刷CDN和浏览器缓存.

直接修改网页回退js版本号就可以了.

爬虫隔离

爬虫流量可达到1/5, 一般情况给爬虫返回简单数据, 比如缓存数据

可以通过这些方法过滤爬虫请求

  1. user-agent
  2. IP重复访问量
  3. 针对公司统一出口IP, 可以给请求种cookie, 如果cookie相同, 可能是爬虫. 可以进行验证码.

热点隔离

秒杀, 促销等可预测的热点访问, 应该放在独立服务器上.

读热点: 多级缓存

使用一致性哈希优化缓存命中率可能会产生热点, 进而击垮单点产生雪崩效应(连续击垮后续节点). 解决办法是通过虚拟节点把每个服务器拆分后交错插入哈希环.

写热点: 缓存+队列, 最终一致性

硬件资源隔离

  • 磁盘读写频繁的应用, 可以专盘专用
  • 运算型的应用, 可以绑定cpu.
    1. 使用taskset命令给进程分配cpu, 还可以查看当前进程的cpu affinity情况
    2. k8s静态cpu分配: kubelet 启动参数--cpu-manager-policy=static --kube-reserved=cpu=1(给k8s系统进程保留的cpu). 同时要把pod 的CPU request和limit相等, 这样才是Guaranteed的pod. 详见https://kubernetes.io/zh/docs/tasks/administer-cluster/cpu-management-policies/
  • 网络IO型的应用, 可以绑定网卡IRQ
  • 其实就是VM直通硬件

服务资源隔离

独立的Redis, DB很常见, 对其它服务依赖性强也可以独立提供, 绑定部署

Hystrix线程池隔离

Hystrix使用独立的线程池来隔离失败操作. Hystrix还可以用来实现服务降级, 熔断.

Servlet 3异步请求线程池

Servlet 3支持AsycContex和AsyncResponse. 可以用来自建线程池, 这样Tomcat线程池只用来把请求放到自有线程池.

请求限流

限流分类

  1. 限制运行中的并发数
  2. 限制瞬时进入的请求量
  3. 限制时间窗口内的平均数量
  4. 限制外调请求避免处罚rate limit

限流算法

  1. 令牌桶: 令牌桶上限固定, 生成令牌速率固定. 请求拿到令牌就执行, 没有令牌就限流. 可以一次拿多个令牌.
    1. 令牌桶允许突发请求, 还有流量整形的能力, 突发请求能多获取一些资源, 但持续大量请求, 就会被限流.
  2. 漏桶: 漏桶流出速度固定, 总量固定. 放入后等待被漏出执行. 桶满了就限流.
    1. 漏桶不允许突发请求, 但有一定的突发容量.
  3. 计数器: 资源总量固定, 用量到了就限流, 运行完成释放计数器资源.

限流技术方案

  1. 接入层限流
    1. Nginx: limit_conn, limit_req
    2. Tomcat线程池: maxConnections, maxThreads
  2. 应用内限流
    1. Guava: timeout cache.
    2. Guava: RateLimiter (令牌桶)
    3. Hystrix信号量

分布式限流

核心是共享流量计数器, 原子计数CAS. 一般用Cache, 比如Redis配合Lua脚本实现原子操作. Coherence有类似功能

应用层和接入层都可以集成Redis实现根据业务逻辑的限流.

  • 接入层Nginx+Lua
  • 应用层Jedis用eval命令把Lua脚本提交到Redis执行, CAS修改计数器

接入层限流

这里讲的是脱离业务逻辑, 根据请求特征进行限流, 比如IP rate limiter

  1. Nginx限流模块: ngx_http_limit_conn_module
  2. Nginx漏桶模块: limit_req, 根据请求参数限流
  3. Nginx网络流量限流: limit_rate
  4. Nginx + Lua + lua_shared_dict, 自己实现限流逻辑, 在content阶段直接返回请求
  5. OpenResty模块: lua-resty-limit-traffic

服务降级

降级预案

  1. 故障等级, 根据SLA影响划分. 一般(抖动), 警告(SLA <95), 错误(SLA <90), 严重错误(突发情况)

降级分类

  1. 页面降级: 关闭单页面, 接入层屏蔽请求
  2. 页面区域降级: 关闭某功能区, JS预置屏蔽脚本
  3. 页面异步请求: 相关功能区同步改异步, JS预置异步脚本
  4. 服务降级: 某服务失效返回空, 后台屏蔽, 前台预置兜底内容
  5. 读写降级: 读缓存, 异步写
  6. 爬虫降级: 读缓存
  7. 风控降级: 特殊用户黑名单, 特定返回内容, 应用层屏蔽

自动降级

  1. 失败计数器触发自动降级
  2. 指数级等待重试
  3. 限流降级: 后台触发限流, 前台显示排队或兜底信息, 比如无货

人工开关降级

  1. 在服务不可用或出现错误时, 人工修改配置屏蔽功能
  2. 配置可存储在配置中心, 数据库, configmap或推送给前端app

读写降级

  1. 读可以只读缓存, 还可以转为静态化页面
  2. 写可以写到Cache或Queue. 如果写Queue还是太慢, 就写到文件日志. 后期由worker更新数据库.
  3. 关闭前台写入口. 比如隐藏评论按钮

尽早降级

在链路的前端降级可以保护更多后续服务

页面应设计降级框架, 各个部分都可以动态配置只读, 隐藏, 异步获取, 显示静态兜底数据等功能.

Nginx直接降级返回请求, 处理速度比应用服务器快几十倍.

Hystrix降级熔断

超时重试注意事项

  1. 重视分布式调用的第三种状态, 没有响应
  2. 写操作谨慎重试, 确保幂等
  3. 重试应遵循指数级后退策略, 防止进一步压垮被调服务
  4. 如果长时间重试失败, 后续请求应启用快速失败策略, 节约时间
  5. 跨机房重试策略容灾
  6. 各级调用都要设置超时时间, 默认设置往往过长, 比如30秒...
    1. Ajax, Nginx, Redis, Queue, NoSQL, 代理服务器, DNS, 外部API
    2. MySQL慢查询: slow_query_log = 1
    3. Tomcat线程池:
      1. connectionTimeout
      2. asyncTimeout异步请求响应超时, 可绑定错误处理Listener

回滚机制

分布式事务回滚

  1. 二阶段, 三阶段提交
  2. 延迟投递消息队列
  3. 补偿服务(执行/撤销), 阿里Seata分布式事务协调器通过截取JDBC请求, 透明实现回滚
  4. TCC模式(预占/确认/取消)
  5. Sagas(提交/重试/撤销)
  6. https://blog.csdn.net/xiaofeng10330111/article/details/86772650

分布式事务回滚的错误处理

  1. 应用自检: pod把事务操作日志保存到外部存储, 如果遇到异常重启, 首先扫描日志文件重做中断的任务
  2. worker定期(分钟, 小时, 天)扫描: 各服务在表内记录对冲数据, worker定期检查修补
  3. 人工对账

部署版本回滚

  1. 小版本增量发布: 先放一台, 逐步切换.
  2. 大版本灰度发布: 并行运行一段时间, AB测试, 发现问题.

数据版本回滚

  1. 重要数据备份修改历史, 以备回滚或审计.
  2. 定期移动数据到归档库还可改善性能, 比如按日期分区, 然后直接删除分区

静态文件按版本发布

  1. 静态文件保存在CDN和客户端缓存, 很难清理
  2. 有回滚需求的静态文件(比如js/css), 文件名带版本号发布, 回滚只要使用原来的名字.
  3. 如需清理静态文件缓存, 在请求连接上加上随机数运行一段时间.

压力测试

  1. 可以在线上部署一套压测专用集群
  2. 用tcpcopy复制真实流量, 扩大数倍后导入压测集群. 直接用真实业务请求压测非常危险.
  3. 压测数据应该分散, 包括长尾数据, 冷门数据.
  4. 排查MySQL慢查询
  5. 压测应该考虑突发情况和未来流量增长. 可用去年同期流量来估计.

应急预案

  1. 故障等级, 根据SLA影响划分. 一般(抖动), 警告(SLA <95), 错误(SLA <90), 严重错误(突发情况)
  2. 全链路分析, 路径日志追查
  3. 配置监控报警
  4. 系统分级: 交易系统, 核心系统, 管理系统, 外围系统
  5. 系统负责人
  6. 值班计划
  7. fix流程: 切换服务, 限流, 降级, datafix, hotfix
  8. 故障总结
  9. 预案演习

高并发

独立缓存

缓存位置

  1. 内核: 寄存器, L1, L2. 依靠锁定核心优化命中率.
  2. CPU共享缓存L3: 这个只要是路由到相同pod就可以, 和JVM缓存优化同理. 一致性哈希, 防止pod频繁上下线.
  3. Java进程: Guava, ehcache. 堆内, 堆外(OHC, Ehcache3) 一致性哈希, ip hash, 命中pod即可.
  4. Node内存: 本地Redis. 本地多进程共享. 由于同一应用多分散部署, 可能适用与关联业务共享缓存. 需要用到pod affinity做关联部署, 可用性差.
  5. 缓存服务: 远程访问Redis, Memcached, 多为集群部署. 方便同一数据中心不同节点上的pod共享信息. 还适用于缓存层静态化(nginx读缓存静态数据, app更新缓存).
  6. CDN: 所有静态资源都可以通过CDN分发. 可能变化的静态资源要按版本号命名, 修改时不需要清理CDN
  7. 浏览器缓存: 这个和CDN一样不好管理, 会变动的数据按版本发布比较好修改. 或者链接后加随机数清理.
  8. app缓存: 可以预先推送一些资源文件, 缓解突发压力.

JVM对象引用

  1. 软引用: 内存不足时才回收
  2. 弱引用: GC时发现就回收
  3. 虚引用: 和弱引用强度一样, 是用来控制垃圾回收行为的, 声明虚引用时要指定一个ReferenceQueue, GC回收虚引用时会把对象放在这个队列里, 程序可以自定义回收操作.

回收算法

  1. FIFO
  2. LRU: 最近最少访问, 最常用的算法
  3. LFU: 最少次被访问, 统计次数属于加权LRU

本地缓存

  1. 堆内: Guava, Ehcache, MapDB
  2. 堆外: Caffinitas.OHC, Ehcache3. 需要配置堆外内存大小 -XX:MaxDirectMemorySize=10G
  3. 磁盘: MapDB, Ehcache, RocksDB

4. 本地缓存的问题:

  1. 单机容量的问题
  2. 数据共享/命中率的问题
  3. 数据一致性的问题

分布式缓存

  1. Redis
  2. Ehcache-clustered + Terracotta Server

多级缓存

有些缓存支持内存+磁盘分级缓存, 比如MapDB

Cache-Aside

业务代码显式调用缓存, 比如读时先读缓存, 写后更新/失效缓存这种代码都写在应用程序里.

  1. 并发更新问题:
    1. 写场景选择更原子的更新点, 比如订阅数据库binlog, 这样只有事务提交后才会更新缓存
    2. 读场景可用一致性哈希归并相同来源的请求, 减少并发几率.

Cache-Through

业务代码只读写缓存, 数据库的读取和写入由缓存代理. 一般实现缓存的读写接口.

分为直读, 直写和异步写三种. 代理写盘听起来很不靠谱.

Guava支持Read-Through, Ehcache支持各种.

引用复制

缓存直接引用原始堆数据, 如果原始数据被修改, 缓存记录也被修改.

Ehcache提供copier接口deep copy缓存数据防止这种错误. Guava不支持.

HTTP缓存

浏览器会把服务器返回的网页缓存在本地, 下次请求时会带上上次返回的时间.

如果服务器返回304代表内容没有改变, 浏览器会直接显示本地缓存的页面.

关键的header

  1. Last-Modified: 服务器返回的文档最后修改时间, 浏览器下次请求时会带上, 供服务器判断是否需要更新.
  2. Expires: 服务器给出的缓存到期时间, 在这个时间内浏览器会直接使用本地缓存, 除非ctrl+F5强制刷新.
  3. Cache-Control: max-age= 服务器给出的缓存最大存活时间, Http/1.1版本, 优先级比Expires高. (Expires = 当前时间 + max-age)
  4. Age: 缓存内容在CDN服务器生存了的时间.
  5. Vary: CDN用来判断返回数据版本的, 一般根据浏览器类型和是否启用gzip压缩来区分.
  6. Via: 从那层返回的缓存, 比如CDN或服务器.
  7. ETag: 是内容的摘要, 给服务器用来做内容比对. 一般用于图片, js, css文件.

代理层静态缓存 Nginx Proxy Cache

  1. 服务端开启浏览器缓存过期时间
  2. Nginx服务器设置本地缓存(proxy_cache模块, 类似浏览器缓存)
  3. 客户端请求到达Nginx时, Nginx按浏览器缓存的过期策略使用本地缓存响应.
  4. 这样即便大量客户端初次访问, 其实都在Nginx层按浏览器缓存返回了. 服务端收到的请求很少.
  5. 这个缓存是保存在Nginx服务器的磁盘上的, 可以使用tmpfs内存文件系统或SSD来提高缓存读写速度.
  6. 代理层静态缓存的命中率会被多实例分散, 可以使用一致性哈希增加命中率, 但会出现单页面热点问题. 解决方案是:
    1. 负载较低用一致性哈希增加命中率
    2. 出现热点降级为轮询分散压力
    3. 将热点数据推送到接入层, 特殊情况, 比如秒杀.
  7. nginx proxy_cache 缓存配置 https://blog.csdn.net/dengjiexian123/article/details/53386586/

清理缓存

如果需要紧急清理Nginx缓存可以使用ngx_cache_purge插件.

使用经验

  1. 只缓存正常请求, 比如200的. 其它情况可能会随时变化.
  2. 缓存key要设置合理, 比如取代排序参数, 才能归并相同请求.
  3. 内容页面实时性不高, 可以设置几秒钟的Nginx静态缓存, 减轻服务器压力.
  4. 本身就是静态内容的, 比如JS/CSS/image可以设置很久的静态缓存, 通过版本进行变更. 当然CDN更好.
  5. 数据页面由于需要实时反映修改, 不宜使用静态缓存, 可以使用分布式缓存订阅更新.
  6. 使用tmpfs内存文件系统或SSD来提高缓存读写速度.
  7. 使用一致性哈希来提高缓存命中率.
  8. 性能测试时要考虑缓存的影响.
  9. 可以在http header里添加调试信息, 比如物理服务器ID.

多级缓存

多级缓存架构

  1. 接入层Nginx, 不带任何业务逻辑, 保持稳定. 一般用RR或Consistent Hash把请求转发到下级Nginx.
    1. K8s Nginx Ingress Controller的一致性哈希是通过annotation: nginx.ingress.kubernetes.io/upstream-hash-by
  2. http内容缓存, Varnish或者Squid
  3. 应用层Nginx, 负责读取缓存, 根据request_uri hash在本地查找缓存(Lua_shared_dict, Nginx Proxy Cache, Local Redis). 如果找到直接返回内容, 对重复请求直接返回304节省流量.
    1. 应用层Nginx的增加会分散缓存的命中率, 可以在接入层使用一致性哈希, 但可能会存在单页面热点问题.
  4. 分布式缓存Redis, 负责维护应用层运算结果. 如果应用层Nginx本地缓存没有命中, 就会尝试查找分布式缓存, 如果找到会同时更新本地缓存.
  5. 回源应用服务器, 应用层Nginx无法找到任何有效缓存, 只能查询应用服务器, 仍然会带上Last-Modified供服务器最终判断是否需要更新.
  6. 应用服务器, 首选查找本地各级缓存(堆内外, 磁盘), 如果仍然没有命中, 运行业务逻辑, 访问数据库. 返回结果并异步写入本地和分布式缓存. 这样所有Nginx都能读到缓存了.
  7. 请求打到应用服务器时可能会有并发更新问题, 参考下面的缓存更新原子性部分
  8. 三级缓存对应的问题
    1. 应用Nginx本地缓存: 解决热点数据重复访问.
    2. 分布式缓存: 减少回源率
    3. 应用服务器本地缓存: 减少缓存穿透后的冲击.

构建缓存式网站

对高频读取的内容服务, 大量请求都是在缓存层处理的.

如果应用层能及时把变化发布到缓存,Nginx可以只从缓存读取数据(设置缓存不过期)

这就是运行在缓存上的网站.

应用要能够主动监听数据变更来更新缓存. 主要依靠订阅消息队列来实现.

如果数据量不大, 也可以考虑定期全量更新缓存.

应用场景: 商品, 订单, 用户, 分类, 价格等访问频率高, 允许一定更新延迟的页面.

缓存优化

  1. 应尽量使用更多的缓存, 缓存空间不够是可以用LRU淘汰. 一般页面都可以接受几秒钟的更新延迟.
  2. 缓存应预加载或主动加载才能有更好的响应速度. 懒加载一般用于缓存API结果, 一定时间后过期.
  3. 增量更新: 如果整页缓存数据较大, 经常更新损耗性能. 可以拆成小模块单独更新.
  4. 大值缓存: Redis单线程读写大数据会卡, 建议值太大拆分成几条存储.
  5. 热点缓存: 读取频繁的数据可以分散到多个slave缓存服务器, 分散到多机房增加访问速度.

缓存更新原子性

  1. 使用Redis Lua脚本保证CAS
  2. 订阅MySQL Binlog依靠数据库写入触发更新, Canal
  3. 使用队列序列化更新操作
  4. 使用Redis分布式锁

缓存崩溃的修复

大流量网站的读操作高度依赖缓存, 缓存崩溃对可用性影响很大.

  1. 内存缓存应该有磁盘备份, 比如Redis是写盘的. 这样宕机只需读磁盘恢复较快.
  2. 使用冗余服务器顶上去.
  3. 降级关闭部分功能, 减少压力.
  4. 在接入层隔离的情况下, 使用Worker预热缓存(加载数据到缓存).

池化

本部分包括线程池, 连接池等. 也包括通用的容器池框架apache commons-pool 2.

合理池化可以节省构建资源, 创建连接的时间.

池化的主要优势是保留长期存活的资源, 主要问题是退出程序时一定要通知远端取消对长期资源的占用.

HTTP连接池

  1. 开启长连接的HTTP连接池才有必要
  2. JVM停止时一定要关闭连接池释放连接.
  3. Http/1.0要求Header带Connection:Keep-Alive才能保持长连接, Http/1.1默认长连接

线程池

  1. 64位系统每线程默认分配1MB线程栈, 可以通过xss改小.
  2. Java获取CPU内核数Runtime.getRuntime().avaliableProcessors();
  3. 线程池的max size太大导致想成太多会OOM或卡死cpu
  4. 线程池的等待队列过长或者使用无界队列会导致插入对象太多OOM

Tomcat线程池

Tomcat使用线程池来处理请求, 默认200. 可以修改配置或在自己的线程池异步处理HTTP请求.

  1. acceptCount: 等待队列大小, 默认100. 超过会拒绝连接
  2. maxConnections: 最大并发HTTP连接数, BIO=maxThreads. NIO:10000, ARP: 8192.
  3. maxThreads: 最大线程数, 默认200. 使用长连接时maxConnections要大于maxThreads.

异步/并发

在应用程序内, 同时调用多个外部服务以节省时间.

在这里使用线程池和client的方式管理和包装请求

  1. 异步: Tomcat内使用自建线程池, 把请求异步化后分级处理, 方便管理
  2. 并发: 封装外部API client来管理外调请求.
    1. 缓存结果: 对请求结果进行缓存.
    2. 请求合并: 对单挑数据的API请求短时间汇集后, 转化为bulk API的调用. 通过Hystrix接口支持.

扩容

系统演化路径

单机 -> 负载均衡 + 集群 -> 微服务 + 缓存 + 队列

扩容顺序

  1. 垂直扩容: 加CPU/内存, HDD -> SDD -> PCIe SSD, 万兆网卡.
  2. 水平扩容: 加机器, 前置接入层负载均衡.
  3. 微服务: 按业务拆分, 方便升级和维护. 降低单模块复杂的, 方便优化.
  4. 服务治理: SOA服务注册发现, K8S部署管理路由.
  5. 接入层: 应用层Nginx: 缓存, 限流, 防刷
  6. CDN: 优化异地访问速度, 分发静态资源

数据库拆分

  1. 垂直拆分(分库): 把不同的表放在不同的数据库里. 缺点是连表查询不能跨库, 解决办法是把需要关联查询的数据抽取到专用的数据库里查询, 或放入ES搜索(异构).
  2. 水平拆分(分表): 把同一个表的数据拆分放在不同的数据库里. 划分策略可以是根据ID, 用户, 时间等. 取模, 哈希.
  3. 读写分离(主从).
  4. NoSQL: 需要频繁读取的, 不涉及复杂查询的数据库, 可以放在KV数据库. 比如购物车.

数据库拆分的问题

  1. 连表查询问题: 按常用查询条件异构聚合, 同步到ES搜索.
  2. 自增ID: 不同库不同自增步长或分布式ID生成服务, 或UUID
  3. 分布式事务: TCC, Sagas, 最终一致性

Redis集群使用经验

  1. 更新Redis数据
    1. 主从: 实现简单, 主备切换麻烦
    2. 双写: 双主, 写入性能差, 不适合异地多机房.
    3. 异步双写: 通过消息队列订阅异步双写. 实时性差点

分库分表实现方案

  1. 中间件层, 数据库访问通过中间件服务实现: Atlas, Cobar, Mycat
  2. JDBC层, 灵活性高, 侵入性少: sharding-jdbc, cobar-client.

分布式事务错误处理

  1. 柔性事务(最大努力尝试): 反复重试 -> 最大次数 -> 失败日志 -> 人工干预

数据异构

  1. 按页面聚合: 把展示某页面需要查询的数据聚合到一起, 一般存储到KV数据库. 这种数据是全量预处理的, 和缓存层有所区别.
  2. 按查询异构: 把关键查询涉及的结果列展平到专用的数据库里, 方便进行复杂SQL查询.

队列

消息队列可以用来: 异步处理, 系统解耦, 数据同步, 流量削峰, 排队限流

数据同步/系统解耦

  1. 应用收到更新请求, 首先写数据库DB
  2. 下面两个步骤可以在应用内做, 也可以用Canal订阅数据库Binlog异步来做.
    1. 把变更发布到消息队列, 供外部系统监听更新
    2. 更新本地缓存和分布式缓存, 供自系统页面服务使用.
    3. 不要在数据库事务或任何同步代码块内发队列消息或更新分布式缓存, I/O操作可能被阻塞.

排队限流

  1. 请求到达放入队列
  2. 拉取队列处理请求
  3. 遇到限流, rate limit, 资源不够, 把请求重新放入队列
  4. 循环上述步骤.

数据库总线队列

通过订阅数据库的binlog, 对数据更改选择性同步到其它数据库, 比如异构库.

防止消息堆积

为防止消息队列被撑爆, 应用可以把堆积的消息取出放在Redis内.

Redis可以存储亿级的消息, 但消息从队列取出后就要Acknowledge了.

可以在Redis内根据消息处理阶段放在不同的List下(待处理, 处理中, 处理失败)

本地消息存储实例

Queue + Redis + Disruptor + 线程池

  1. 拉取消息放入Redis List待处理
  2. key放入Disruptor
  3. 使用Disruptor event worker排队处理拉取的消息
  4. 成功就从Redis内删除, 失败就移入处理失败.
  5. 对Redis内数据的操作使用Lua脚本原子化

使用Canal监听数据库Binlog

  1. Canal Server: 伪装MySQL slave接收Binlog
  2. Canal Client: 处理Binlog执行用户逻辑, 比如发布到消息队列或Redis.
  3. 好处是对写操作的AOP监听.

相关文章

  • Amazon Dynamo几乎实践了所有分布式理论

This article is licensed with Creative Commons Attribution-NonCommercial-No Derivatives 4.0 International License
Tag: Cache Distributed System HA
Last updated:2023-02-27

Alvin Liu

Software Developer in Toronto

Like

Comments

razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
Cancel

COPYRIGHT © 2024 Alvin Liu. alvingoodliu@[email protected] ALL RIGHTS RESERVED.

Theme Made By Seaton Jiang