Pipeline Pattern + Service Layer 组合架构详解

架构概述

Pipeline Pattern + Service Layer 是一种将复杂业务流程分解为可组合、可重用组件的设计模式组合。它将传统的面向过程代码转换为面向对象的、高度模块化的架构。

核心设计理念

1. 分离关注点 (Separation of Concerns)

  • Controller Layer: 处理HTTP请求/响应
  • Service Layer: 封装业务逻辑
  • Pipeline Layer: 管理处理流程
  • Processor Layer: 实现具体的处理步骤

2. 单一职责原则 (Single Responsibility Principle)

  • 每个Processor只负责一个特定的处理步骤
  • 每个Service只负责一个业务领域
  • Pipeline只负责流程编排

3. 开闭原则 (Open/Closed Principle)

  • 对扩展开放:可以轻松添加新的Processor
  • 对修改封闭:不需要修改现有代码

架构层次详解

Layer 1: Controller Layer (控制层)

1
2
3
4
5
6
7
8
9
10
11
// 职责:处理HTTP请求,参数验证,响应格式化
func FlashSaleListV2(ctx *logic.Context) {
req := ctx.GetRequest().(*aggregateCmd.FlashSaleListReq)

// 委托给Service层处理业务逻辑
service := NewFlashSaleService()
resp, errCode := service.GetFlashSaleList(context.Background(), req)

// 设置响应
ctx.SetResponse(resp)
}

特点

  • 薄薄的一层,不包含业务逻辑
  • 负责请求/响应的格式转换
  • 处理框架相关的逻辑

Layer 2: Service Layer (服务层)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type FlashSaleService interface {
GetFlashSaleList(ctx context.Context, req *aggregateCmd.FlashSaleListReq) (*aggregateCmd.FlashSaleListResp, errors.ErrorCode)
}

type flashSaleService struct {
pipeline Pipeline // 依赖Pipeline来处理具体流程
}

func (s *flashSaleService) GetFlashSaleList(ctx context.Context, req *aggregateCmd.FlashSaleListReq) (*aggregateCmd.FlashSaleListResp, errors.ErrorCode) {
// 1. 创建处理上下文
fsCtx := &FlashSaleContext{Request: req}

// 2. 执行处理管道
if errCode := s.pipeline.Execute(ctx, fsCtx); !errors.Ok.EqualCode(errCode) {
return nil, errCode
}

// 3. 构建响应
return s.buildResponse(fsCtx), errors.Ok
}

特点

  • 定义业务接口
  • 管理事务边界
  • 处理业务异常
  • 不包含具体的处理逻辑

Layer 3: Pipeline Layer (管道层)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type Pipeline interface {
AddProcessor(processor Processor) Pipeline
Execute(ctx context.Context, fsCtx *FlashSaleContext) errors.ErrorCode
}

type flashSalePipeline struct {
processors []Processor
}

func (p *flashSalePipeline) Execute(ctx context.Context, fsCtx *FlashSaleContext) errors.ErrorCode {
for _, processor := range p.processors {
if errCode := processor.Process(ctx, fsCtx); !errors.Ok.EqualCode(errCode) {
return errCode
}
}
return errors.Ok
}

特点

  • 管理处理器的执行顺序
  • 统一的错误处理
  • 支持流程编排
  • 可插拔的处理器架构

Layer 4: Processor Layer (处理器层)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type Processor interface {
Process(ctx context.Context, fsCtx *FlashSaleContext) errors.ErrorCode
Name() string
}

// 具体处理器示例
type PromotionDataProcessor struct{}

func (p *PromotionDataProcessor) Process(ctx context.Context, fsCtx *FlashSaleContext) errors.ErrorCode {
// 实现具体的处理逻辑
// 从营销服务获取数据
// 设置到fsCtx中
return errors.Ok
}

特点

  • 实现具体的处理逻辑
  • 可独立测试
  • 可重用
  • 职责单一

数据流转模式

Context Pattern (上下文模式)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type FlashSaleContext struct {
// Input - 输入数据
Request *aggregateCmd.FlashSaleListReq

// Intermediate - 中间数据,各处理器间传递
OriginalPromotionItems []*promotionCmd.ActivityItem
FilteredPromotionItems []*promotionCmd.ActivityItem
LSItemList []*lsitemcmd.Item

// Output - 输出数据
FlashSaleItems []*aggregateCmd.FlashSaleItem
FlashSaleBriefItems []*aggregateCmd.FlashSaleBriefItem
Session *aggregateCmd.FlashSaleSession
}

数据流转过程

  1. Controller创建初始Context
  2. 每个Processor读取Context中的数据
  3. Processor处理后更新Context
  4. 下一个Processor继续处理
  5. Service层从Context构建最终响应

架构优势分析

1. 可测试性 (Testability)

1
2
3
4
5
6
7
8
9
10
11
12
13
// 单元测试示例
func TestPromotionDataProcessor(t *testing.T) {
processor := NewPromotionDataProcessor()
ctx := context.Background()
fsCtx := &FlashSaleContext{
Request: mockRequest,
}

errCode := processor.Process(ctx, fsCtx)

assert.Equal(t, errors.Ok, errCode)
assert.NotEmpty(t, fsCtx.OriginalPromotionItems)
}

2. 可扩展性 (Extensibility)

1
2
3
4
5
6
7
8
9
10
11
// 添加新功能只需要新增Processor
type CacheProcessor struct{}
type MetricsProcessor struct{}
type ValidationProcessor struct{}

// 在管道中组合
pipeline := NewFlashSalePipeline().
AddProcessor(NewValidationProcessor()). // 验证
AddProcessor(NewCacheProcessor()). // 缓存
AddProcessor(NewPromotionDataProcessor()). // 原有逻辑
AddProcessor(NewMetricsProcessor()) // 监控

3. 可维护性 (Maintainability)

  • 代码职责清晰:每个组件职责单一
  • 错误处理统一:Pipeline层统一处理
  • 日志记录一致:每个Processor都有统一的日志格式

4. 可重用性 (Reusability)

1
2
3
4
5
6
7
8
// Processor可以在不同的Pipeline中重用
flashSalePipeline := NewFlashSalePipeline().
AddProcessor(NewPromotionDataProcessor()).
AddProcessor(NewItemFilterProcessor())

discountPipeline := NewDiscountPipeline().
AddProcessor(NewPromotionDataProcessor()). // 重用
AddProcessor(NewDiscountCalculateProcessor())

设计模式应用

1. Strategy Pattern (策略模式)

1
2
3
4
5
6
7
8
9
10
11
// 不同的排序策略
type SortStrategy interface {
Sort([]*aggregateCmd.FlashSaleItem) []*aggregateCmd.FlashSaleItem
}

type DiscountFirstStrategy struct{}
type StockFirstStrategy struct{}

type FlashSaleSortProcessor struct {
strategy SortStrategy
}

2. Chain of Responsibility (责任链模式)

1
2
3
// 每个Processor形成一个责任链
// 请求在链中传递,每个节点都可以处理
Validation → DataFetch → Filter → Assembly → Sort

3. Template Method Pattern (模板方法模式)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 基础Processor提供模板
type BaseProcessor struct{}

func (p *BaseProcessor) Process(ctx context.Context, fsCtx *FlashSaleContext) errors.ErrorCode {
// 模板方法
if err := p.preProcess(ctx, fsCtx); err != nil {
return err
}

if err := p.doProcess(ctx, fsCtx); err != nil {
return err
}

return p.postProcess(ctx, fsCtx)
}

4. Decorator Pattern (装饰器模式)

1
2
3
4
5
6
7
8
// 为Processor添加额外功能
type LoggingProcessor struct {
wrapped Processor
}

type MetricsProcessor struct {
wrapped Processor
}

性能优化策略

1. 并行处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
type ParallelPipeline struct {
processors [][]Processor // 二维数组,支持并行执行
}

func (p *ParallelPipeline) Execute(ctx context.Context, fsCtx *FlashSaleContext) errors.ErrorCode {
for _, parallelGroup := range p.processors {
// 并行执行同一组的Processor
errChan := make(chan errors.ErrorCode, len(parallelGroup))
for _, processor := range parallelGroup {
go func(proc Processor) {
errChan <- proc.Process(ctx, fsCtx)
}(processor)
}

// 等待所有并行任务完成
for i := 0; i < len(parallelGroup); i++ {
if errCode := <-errChan; !errors.Ok.EqualCode(errCode) {
return errCode
}
}
}
return errors.Ok
}

2. 缓存策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type CacheProcessor struct {
cache Cache
ttl time.Duration
}

func (p *CacheProcessor) Process(ctx context.Context, fsCtx *FlashSaleContext) errors.ErrorCode {
key := p.buildCacheKey(fsCtx.Request)

// 尝试从缓存获取
if cached := p.cache.Get(key); cached != nil {
fsCtx.FlashSaleItems = cached.Items
return errors.Ok
}

// 缓存未命中,继续处理
return errors.Ok
}

3. 熔断器模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type CircuitBreakerProcessor struct {
wrapped Processor
circuitBreaker CircuitBreaker
}

func (p *CircuitBreakerProcessor) Process(ctx context.Context, fsCtx *FlashSaleContext) errors.ErrorCode {
if p.circuitBreaker.IsOpen() {
return errors.ErrorServiceUnavailable
}

errCode := p.wrapped.Process(ctx, fsCtx)

if !errors.Ok.EqualCode(errCode) {
p.circuitBreaker.RecordFailure()
} else {
p.circuitBreaker.RecordSuccess()
}

return errCode
}

监控和可观测性

1. 指标收集

1
2
3
4
5
6
7
8
9
10
11
type MetricsCollector interface {
RecordProcessorLatency(processorName string, duration time.Duration)
RecordProcessorError(processorName string, errorCode string)
RecordPipelineExecution(pipelineName string, itemCount int)
}

type PrometheusMetricsCollector struct{}

func (p *PrometheusMetricsCollector) RecordProcessorLatency(processorName string, duration time.Duration) {
processorLatencyHistogram.WithLabelValues(processorName).Observe(duration.Seconds())
}

2. 链路追踪

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type TracingProcessor struct {
wrapped Processor
tracer opentracing.Tracer
}

func (p *TracingProcessor) Process(ctx context.Context, fsCtx *FlashSaleContext) errors.ErrorCode {
span, ctx := opentracing.StartSpanFromContext(ctx, p.wrapped.Name())
defer span.Finish()

span.SetTag("processor.name", p.wrapped.Name())
span.SetTag("request.platform", fsCtx.Request.GetPlatform())

errCode := p.wrapped.Process(ctx, fsCtx)

if !errors.Ok.EqualCode(errCode) {
span.SetTag("error", true)
span.LogFields(log.String("error.code", fmt.Sprintf("%d", errCode.Code)))
}

return errCode
}

适用场景

适合使用的场景:

  1. 复杂的数据处理流程:需要多个步骤的数据转换
  2. 需要灵活配置的业务流程:不同场景需要不同的处理步骤
  3. 高度可测试的代码:需要单元测试覆盖率
  4. 团队协作开发:不同开发者可以并行开发不同的Processor
  5. 需要监控和调试:需要了解每个步骤的执行情况

不适合的场景:

  1. 简单的CRUD操作:过度设计
  2. 性能要求极高的场景:可能引入额外开销
  3. 变化很少的稳定流程:增加复杂性

最佳实践

1. Processor设计原则

  • 无状态:Processor应该是无状态的
  • 幂等性:相同输入应该产生相同输出
  • 快速失败:尽早发现并报告错误

2. Context设计原则

  • 不可变性:尽量避免修改已设置的数据
  • 清晰命名:字段名要清楚表达含义
  • 类型安全:使用强类型而不是interface{}

3. Pipeline设计原则

  • 顺序重要:Processor的顺序要有逻辑意义
  • 错误传播:错误要能正确向上传播
  • 资源管理:确保资源得到正确释放

业务引擎

  • 对于简单的接口逻辑可以直接写过程代码
  • 复杂接口可以考虑使用责任链的方式
  • 复杂度更高的代码流程控制的方式

工作流引擎与任务编排

规则引擎与风控、资损、校验

脚本执行引擎与低代码平台

总结

Pipeline Pattern + Service Layer 组合架构通过将复杂的业务流程分解为独立的、可组合的组件,实现了:

  • 高内聚低耦合的模块化设计
  • 易于测试和维护的代码结构
  • 灵活可配置的业务流程
  • 强大的扩展能力重用性

这种架构特别适合处理电商、金融等复杂业务场景,能够显著提升代码质量和开发效率。

前言:怎样的系统算是稳定高可用的

首先回答另一个问题,怎样的系统算是稳定的?

Google SRE中(SRE三部曲[1])有一个层级模型来描述系统可靠性基础和高层次需求(Dickerson’s Hierarchy of Service Reliability),如下图:


该模型由Google SRE工程师Mikey Dickerson在2013年提出,将系统稳定性需求按照基础程度进行了不同层次的体系化区分,形成稳定性标准金字塔模型:

  • 金字塔的底座是监控(Monitoring),这是一个系统对于稳定性最基础的要求,缺少监控的系统,如同蒙上眼睛狂奔的野马,无从谈及可控性,更遑论稳定性。
  • 更上层是应急响应(Incident Response),从一个问题被监控发现到最终解决,这期间的耗时直接取决于应急响应机制的成熟度。合理的应急策略能保证当故障发生时,所有问题能得到有序且妥善的处理,而不是慌乱成一锅粥。
  • 事后总结以及根因分析(Postmortem&Root Caue Analysis),即我们平时谈到的“复盘”,虽然很多人都不太喜欢这项活动,但是不得不承认这是避免我们下次犯同样错误的最有效手段,只有当摸清故障的根因以及对应的缺陷,我们才能对症下药,合理进行规避。
  • 测试和发布管控(Testing&Release procedures),大大小小的应用都离不开不断的变更与发布,有效的测试与发布策略能保障系统所有新增变量都处于可控稳定区间内,从而达到整体服务终态稳定
  • 容量规划(Capacity Planning)则是针对于这方面变化进行的保障策略。现有系统体量是否足够支撑新的流量需求,整体链路上是否存在不对等的薄弱节点,都是容量规划需要考虑的问题。
  • 位于金字塔模型最顶端的是产品设计(Product)与软件研发(Development),即通过优秀的产品设计与软件设计使系统具备更高的可靠性,构建高可用产品架构体系,从而提升用户体验

系统稳定性建设概述


从金字塔模型我们可以看到构建维护一个高可用服务所需要做到的几方面工作:

  • 产品、技术、架构的设计,高可用的架构体系
  • 系统链路&业务策略梳理和维护(System & Biz Profiling)
  • 容量规划(Capacity Planning)
  • 应急响应(Incident Response)
  • 测试
  • 事后总结(Testing & Postmortem)
  • 监控(Monitoring)
  • 资损体系
  • 风控体系
  • 大促保障
  • 性能优化


监控&告警梳理 – Monitoring

站在监控的角度看,我们的系统从上到下一般可以分为三层:业务(Biz)、应用(Application)、系统(System)。系统层为最下层基础,表示操作系统相关状态;应用层为JVM层,涵盖主应用进程与中间件运行状态;业务层为最上层,为业务视角下服务对外运行状态。因此进行大促稳定性监控梳理时,可以先脱离现有监控,先从核心、资损链路开始,按照业务、应用(中间件、JVM、DB)、系统三个层次梳理需要哪些监控,再从根据这些索引找到对应的监控告警,如果不存在,则相应补上;如果存在则检查阈值、时间、告警人是否合理。

监控

监控系统一般有四项黄金指标:延时(Latency), 错误(Error),流量(Traffic), 饱和度(Situation),各层的关键性监控同样也可以按照这四项指标来进行归类,具体如下:


告警

是不是每项监控都需要告警?答案当然是否定的。建议优先设置Biz层告警,因为Biz层我们对外服务最直观业务表现,最贴切用户感受。Application&System层指标主要用于监控,部分关键&高风险指标可设置告警,用于问题排查定位以及故障提前发现。对于一项告警,我们一般需要关注级别、阈值、通知人等几个点。

  1. 级别
    即当前告警被触发时,问题的严重程度,一般来说有几个衡量点:
  • 是否关联NOC
  • 是否产生严重业务影响
  • 是否产生资损
  1. 阈值
  • 即一项告警的触发条件&时间,需根据具体场景合理制定。一般遵循以下原则:
  • 不可过于迟钝。一个合理的监控体系中,任何异常发生后都应触发相关告警。
  • 不可过于敏感。过于敏感的阈值会造成频繁告警,从而导致响应人员疲劳应对,无法筛选真实异常。若一个告警频繁出现,一般是两个原因:系统设计不合理 or 阈值设置不合理。
  • 若单一指标无法反馈覆盖整体业务场景,可结合多项指标关联构建。
  • 需符合业务波动曲线,不同时段可设置不同条件&通知策略。
  1. 通知人&方式
  • 若为业务指标异常(Biz层告警),通知人应为问题处理人员(开发、运维同学)与业务关注人员(TL、业务同学)的集合,通知方式较为实时,比如电话通知。
  • 若为应用 & 系统层告警,主要用于定位异常原因,通知人设置问题排查处理人员即可,通知方式可考虑钉钉、短信等低干扰方式。
  • 除了关联层次,对于不同级别的告警,通知人范围也可适当扩大,尤其是关联GOC故障的告警指标,应适当放宽范围,通知方式也应更为实时直接

应产出数据

完成该项梳理工作后,我们应该产出以下数据:

  1. 系统监控模型,格式同表1
  • Biz、Application、System 分别存在哪些待监控点
  • 监控点是否已全部存在指标,仍有哪些待补充
  1. 系统告警模型列表,需包含以下数据
  • 关联监控指标(链接)
  • 告警关键级别
  • 是否推送GOC
  • 是否产生资损
  • 是否关联故障
  • 是否关联预案
  1. 业务指标大盘,包含Biz层重点监控指标数据
  2. 系统&应用指标大盘,包含核心系统关键系统指标,可用于白盒监控定位问题。

高可用的架构设计

系统链路梳理和维护 System & Biz Profiling

系统链路梳理是所有保障工作的基础,如同对整体应用系统进行一次全面体检,从流量入口开始,按照链路轨迹,逐级分层节点,得到系统全局画像与核心保障点。

入口梳理盘点

一个系统往往存在十几个甚至更多流量入口,包含HTTP、RPC、消息等都多种来源。如果无法覆盖所有所有链路,可以从以下三类入口开始进行梳理:

  • 核心重保流量入口
    • 用户承诺服务SLI较高,对数据准确性、服务响应时间、可靠度具有明确要求。
    • 业务核心链路,浏览、下单、支付、履约
    • 面向企业级用户
  • 资损事件对应入口
    • 关联到公司资金收入或者客户资金收入收费服务
  • 大流量入口
    • 系统TPS&QPS TOP5~10
    • 该类入口虽然不涉及较高SLI与资损要求,但是流量较高,对整体系统负载有较大影响

节点分层判断

对于复杂场景可以做节点分层判断

流量入口就如同线团中的线头,挑出线头后就可按照流量轨迹对链路上的节点(HSF\DB\Tair\HBase等一切外部依赖)按照依赖程度、可用性、可靠性进行初级分层区分。

  1. 强弱依赖节点判断
  • 若节点不可用,链路业务逻辑被中断 or 高级别有损(存在一定耐受阈值),则为业务强依赖;反之为弱依赖。
  • 若节点不可用,链路执行逻辑被中断(return error),则为系统强依赖;反之为弱依赖。
  • 若节点不可用,系统性能受影响,则为系统强依赖;反之为弱依赖。
  • 按照快速失败设计逻辑,该类节点不应存在,但是在不变更应用代码前提下,如果出现该类节点,应作为强依赖看待。
  • 若节点无感可降级 or 存在业务轻微损伤替换方案,则为弱依赖。
  1. 低可用依赖节点判断
  • 节点服务日常超时严重
  • 节点对应系统资源不足
  1. 高风险节点判断
  • 上次大促后,节点存在大版本系统改造
  • 新上线未经历过大促的节点
  • 节点对应系统是否曾经出现高级别故障
  • 节点故障后存在资损风险

应产出数据

  • 识别核心接口(流程)调用拓扑图或者时序图(借用分布式链路追踪系统获得调用拓扑图)
  • 调用比
  • 识别资损风险
  • 识别内外部依赖

完成该项梳理工作后,我们应该产出以下数据:对应业务域所有核心链路分析,技术&业务强依赖、核心上游、下游系统、资损风险应明确标注。

业务策略&容量规划 Capacity Planning - 容量规划

业务策略

不同于高可用系统建设体系,大促稳定性保障体系与面向特定业务活动的针对性保障建设,因此,业务策略与数据是我们进行保障前不可或缺的数据。
一般大促业务数据可分为两类,全局业务形态评估以及应急策略&玩法。

全局评估

该类数据从可以帮助我们进行精准流量评估、峰值预测、大促人力排班等等,一般包含下面几类:

  • 业务量预估体量(日常X倍)
  • 预估峰值日期
  • 大促业务时长(XX日-XX日)
  • 业务场景预估流量分配

应急策略

  • 该类数据指相较于往年大促活动,本次大促业务变量,可用于应急响应预案与高风险节点评估等,一般包含下面两类:
  • 特殊业务玩法

容量规划的本质是追求计算风险最小化和计算成本最小化之间的平衡,只追求任意其一都不是合理的。为了达到这两者的最佳平衡点,需尽量精准计算系统峰值负载流量,再将流量根据单点资源负载上限换算成相应容量,得到最终容量规划模型。

流量模型评估

  1. 入口流量

对于一次大促,系统峰值入口流量一般由常规业务流量与非常规增量(比如容灾预案&业务营销策略变化带来的流量模型配比变化)叠加拟合而成。

  • 常规业务流量一般有两类计算方式:
    • 历史流量算法:该类算法假设当年大促增幅完全符合历史流量模型,根据当前&历年日常流量,计算整体业务体量同比增量模型;然后根据历年大促-日常对比,计算预估流量环比增量模型;最后二者拟合得到最终评估数据。
    • 由于计算时无需依赖任何业务信息输入,该类算法可用于保障工作初期业务尚未给出业务总量评估时使用,得到初估业务流量。
    • 业务量-流量转化算法(GMV\DAU\订单量):该类算法一般以业务预估总量(GMV\DAU\订单量)为输入,根据历史大促&日常业务量-流量转化模型(比如经典漏洞模型)换算得到对应子域业务体量评估。- 该种方式强依赖业务总量预估,可在保障工作中后期使用,在初估业务流量基础上纳入业务评估因素考虑。
  • 非常规增量一般指前台业务营销策略变更或系统应急预案执行后流量模型变化造成的增量流量。例如,NA61机房故障时,流量100%切换到NA62后,带来的增量变化.考虑到成本最小化,非常规增量P计算时一般无需与常规业务流量W一起,全量纳入叠加入口流量K,一般会将非常规策略发生概率λ作为权重
  1. 节点流量
    节点流量由入口流量根据流量分支模型,按比例转化而来。分支流量模型以系统链路为计算基础,遵循以下原则:
  • 同一入口,不同链路占比流量独立计算。
  • 针对同一链路上同一节点,若存在多次调用,需计算按倍数同比放大(比如DB\Tair等)。
  • DB写流量重点关注,可能出现热点造成DB HANG死。

容量转化

节点容量是指一个节点在运行过程中,能够同时处理的最大请求数。它反映了系统的瞬时负载能力。

1)Little Law衍生法则
不同类型资源节点(应用容器、Tair、DB、HBASE等)流量-容量转化比各不相同,但都服从Little Law衍生法则,即:
节点容量=节点吞吐率×平均响应时间

2)N + X 冗余原则

在满足目标流量所需要的最小容量基础上,冗余保留X单位冗余能力
X与目标成本与资源节点故障概率成正相关,不可用概率越高,X越高
对于一般应用容器集群,可考虑X = 0.2N

全链路压测(TODO)

  • 上述法则只能用于容量初估(大促压测前&新依赖),最终精准系统容量还是需要结合系统周期性压力测试得出。

应产出数据

  • 基于模型评估的入口流量模型 & 集群自身容量转化结果(若为非入口应用,则为限流点梳理)。
  • 基于链路梳理的分支流量模型 & 外部依赖容量转化结果。

大促保障

Incident Response - 紧急&前置预案梳理

要想在大促高并发流量场景下快速对线上紧急事故进行响应处理,仅仅依赖值班同学临场发挥是远远不够的。争分夺秒的情况下,无法给处理人员留有充足的策略思考空间,而错误的处理决策,往往会导致更为失控严重的业务&系统影响。因此,要想在大促现场快速而正确的响应问题,值班同学需要做的是选择题(Which),而不是陈述题(What)。而选项的构成,便是我们的业务&系统预案。从执行时机与解决问题属性来划分,预案可分为技术应急预案、技术前置预案、业务应急预案、业务前置预案等四大类。结合之前的链路梳理和业务评估结果,我们可以快速分析出链路中需要的预案,遵循以下原则:

  • 技术应急预案:该类预案用于处理系统链路中,某层次节点不可用的情况,例如技术/业务强依赖、弱稳定性、高风险等节点不可用等异常场景。
  • 技术前置预案:该类预案用于平衡整体系统风险与单节点服务可用性,通过熔断等策略保障全局服务可靠。例如弱稳定性&弱依赖服务提前降级、与峰值流量时间冲突的离线任务提前暂定等。
  • 业务应急预案:该类预案用于应对业务变更等非系统性异常带来的需应急处理问题,例如业务数据错误(数据正确性敏感节点)、务策略调整(配合业务应急策略)等
  • 业务前置预案:该类预案用于配和业务全局策略进行的前置服务调整(非系统性需求)

应产出数据

完成该项梳理工作后,我们应该产出以下数据:

  • 执行&关闭时间(前置预案)
  • 触发阈值(紧急预案,须关联相关告警)
  • 关联影响(系统&业务)
  • 决策&执行&验证人员
  • 开启验证方式
  • 关闭阈值(紧急预案)
  • 关闭验证方式

阶段性产出-全链路作战地图

进行完上述几项保障工作,我们基本可得到全局链路作战地图,包含链路分支流量模型、强弱依赖节点、资损评估、对应预案&处理策略等信息。大促期间可凭借该地图快速从全局视角查看应急事件相关影响,同时也可根据地图反向评估预案、容量等梳理是否完善合理。

Incident Response - 作战手册梳理

作战手册是整个大促保障的行动依据,贯穿于整个大促生命周期,可从事前、事中、事后三个阶段展开考虑。整体梳理应本着精准化、精细化的原则,理想状态下,即便是对业务、系统不熟悉的轮班同学,凭借手册也能快速响应处理线上问题。
事前
1)前置检查事项清单

  • 大促前必须执行事项checklist,通常包含以下事项:
  • 集群机器重启 or 手动FGC
  • 影子表数据清理
  • 检查上下游机器权限
  • 检查限流值
  • 检查机器开关一致性
  • 检查数据库配置
  • 检查中间件容量、配置(DB\缓存\NoSQL等)
  • 检查监控有效性(业务大盘、技术大盘、核心告警)
  • 每个事项都需包含具体执行人、检查方案、检查结果三列数据
    2)前置预案
  • 域内所有业务&技术前置预案。

事中

  1. 紧急技术&业务预案
    需要包含的内容基本同前置预案,差异点如下:
  • 执行条件&恢复条件:具体触发阈值,对应监控告警项。
  • 通知决策人。
  1. 应急工具&脚本
    常见故障排查方式、核心告警止血方式(强弱依赖不可用等),业务相关日志捞取脚本等。
  2. 告警&大盘
  • 应包含业务、系统集群及中间件告警监控梳理结果,核心业务以及系统大盘,对应日志数据源明细等数据:
  • 日志数据源明细:数据源名称、文件位置、样例、切分格式。
  • 业务、系统集群及中间件告警监控梳理结果:关联监控指标(链接)、告警关键级别、是否推送GOC、是否产生资损、是否关联故障、是否关联预案。
  • 核心业务&系统大盘:大盘地址、包含指标明细(含义、是否关联告警、对应日志)。
  1. 上下游机器分组
  • 应包含核心系统、上下游系统,在不同机房、单元集群分组、应用名,可用于事前-机器权限检查、事中-应急问题排查黑屏处理。
  1. 值班注意事项
  • 包含每班轮班同学值班必做事项、应急变更流程、核心大盘链接等。
  1. 核心播报指标
  • 包含核心系统&服务指标(CPU\LOAD\RT)、业务关注指标等,每项指标应明确具体监控地址、采集方式。
  1. 域内&关联域人员通讯录、值班
  • 包含域内技术、TL、业务方对应排班情况、联系方式(电话),相关上下游、基础组件(DB、中间件等)对应值班情况。
  1. 值班问题记录
  • 作战记录,记录工单、业务问题、预案(前置\紧急)(至少包含:时间、问题描述(截图)、影响分析、决策&解决过程等)。值班同学在值班结束前,进行记录。
    事后
  1. 系统恢复设置事项清单(限流、缩容)
    一般与事前检查事项清单对应,包含限流阈值调整、集群缩容等大促后恢复操作。
  2. 大促问题复盘记录
  • 应包含大促遇到的核心事件总结梳理。

沙盘推演和演练 Incident Response

实战沙盘演练是应急响应方面的最后一项保障工作,以历史真实故障CASE作为应急场景输入,模拟大促期间紧急状况,旨在考验值班同学们对应急问题处理的响应情况。
一般来说,一个线上问题从发现到解决,中间需要经历定位&排查&诊断&修复等过程,总体遵循以下几点原则:

  • 尽最大可能让系统先恢复服务,同时为根源调查保护现场(机器、日志、水位记录)。
  • 避免盲目搜索,依据白盒监控针对性诊断定位。
  • 有序分工,各司其职,避免一窝蜂失控乱象。
  • 依据现场情况实时评估影响范围,实在无法通过技术手段挽救的情况(例如强依赖不可用),转化为业务问题思考(影响范围、程度、是否有资损、如何协同业务方)。
  • 沙盘演练旨在检验值班同学故障处理能力,着重关注止血策略、分工安排、问题定位等三个方面:
    国际化中台双11买家域演练
    根据故障类型,常见止血策略有以下解决思路:
  • 入口限流:调低对应Provider服务来源限流值
  • 应对突发流量过高导致自身系统、下游强依赖负载被打满。
  • 下游降级:降级对应下游服务
  • 下游弱依赖不可用。
  • 下游业务强依赖经业务同意后降级(业务部分有损)。
  • 单点失败移除:摘除不可用节点
  • 单机水位飙高时,先下线不可用单机服务(无需下线机器,保留现场)。
  • 应对集群单点不可用、性能差。
  • 切换:单元切流或者切换备份

应对单库或某单元依赖因为自身原因(宿主机或网络),造成局部流量成功率下跌下跌。
Google SRE中,对于紧急事故管理有以下几点要素:

  • 嵌套式职责分离,即分确的职能分工安排
  • 控制中心\作战室
  • 实时事故状态文档
  • 明确公开的职责交接
  • 其中嵌套式职责分离,即分确的职能分工安排,达到各司其职,有序处理的效果,一般可分为下列几个角色:
    事故总控:负责协调分工以及未分配事务兜底工作,掌握全局概要信息,一般为PM/TL担任。
    事务处理团队:事故真正处理人员,可根据具体业务场景&系统特性分为多个小团队。团队内部存在域内负责人,与事故总控人员进行沟通。
    发言人:事故对外联络人员,负责对事故处理内部成员以及外部关注人员信息做周期性信息同步,同时需要实时维护更新事故文档。
    规划负责人:负责外部持续性支持工作,比如当大型故障出现,多轮排班轮转时,负责组织职责交接记录

资损体系

定期review资损风险

事中及时发现


【得物技术】浅谈资损防控

事后复盘和知识沉淀

参考学习

风控体系

性能优化


学习资料:

电商系统整体架构设计

业务流 (business process)


E-commerce process

系统流 (system process)


E-commerce whole process of system

产品架构 (Product Structure/组织架构)


E-commerce product structure

应用架构

技术架构

数据架构

商品管理 Product Center

商品信息包括哪些内容

商品系统的演进

阶段 主要特征/能力 技术架构/数据模型 适用场景/目标 实现方式简单说明
初始阶段 - 商品信息简单,字段少
- SKU/SPU未严格区分
- 价格库存直接在商品表
- 仅支持基本的增删改查
单表/简单表结构 小型电商、业务初期,SKU数量少 单体应用,单表存储,简单业务逻辑,直接数据库操作
成长阶段 - 引入SPU/SKU模型
- 属性、类目、品牌等实体独立
- 支持多规格商品
- 价格库存可拆分为独立表
关系型数据库,ER模型优化 SKU多样化,品类扩展,业务快速增长 关系型数据库,ER模型优化,多表存储,业务逻辑复杂
成熟阶段 - 商品中台化,支持多业务线/多渠道
- 属性体系灵活可扩展
- 多级类目、标签、图片、描述等丰富
- 商品快照、操作日志、版本控制
中台架构,微服务/多表/NoSQL 大型平台,业务复杂,需支撑多业务场景 分布式服务,插件化/配置化流程,状态机驱动,异步消息,灵活数据模型
未来演进 - 多语言多币种支持
- 商品内容多媒体化(视频、3D等)
- AI智能标签/推荐
- 商品数据实时分析与洞察
分布式/云原生/大数据平台 国际化、智能化、数据驱动的电商生态 云原生架构,AI/大数据分析,自动化运维,弹性伸缩,智能路由与风控

什么是SPU、SKU

方案一:同时创建多个SKU,并同步生成关联的SPU。整体方案是直接创建SKU,并维护多个不同的属性;该方案适用于大多数C2C综合电商平台(例如,阿里巴巴就是采用这种方式创建商品)。
方案二:先创建SPU,再根据SPU创建SKU。整体方案是由平台的主数据团队负责维护SPU,商家(包括自营和POP)根据SPU维护SKU。在创建SKU时,首先选择SPU(SPU中的基本属性由数据团队维护),然后基于SPU维护销售属性和物流属性,最后生成SKU;该方案适用于高度专业化的垂直B2B行业,如汽车、医药等。
这两种方案的原因是:垂直B2B平台上的业务(传统行业、年长的商家)操作能力有限,维护产品属性的错误率远高于C2C平台,同时平台对产品结构控制的要求较高。为了避免同一产品被不同商家维护成多个不同的属性(例如,汽车轮胎的胎面宽度、尺寸等属性),平台通常选择专门的数据团队来维护产品的基本属性,即维护SPU。
此外,B2B垂直电商的品类较少,SKU数量相对较小,品类标准化程度高,平台统一维护的可行性较高。
对于拥有成千上万品类的综合电商平台,依靠平台数据团队的统一维护是不现实的,或者像服装这样非标准化的品类对商品结构化管理的要求较低。因此,综合平台(阿里巴巴和亚马逊)的设计方向与垂直平台有所不同。
实际上,即使对于综合平台,不同的品类也会有不同的设计方法。一些品类具有垂直深度,因此也采用平台维护SPU和商家创建SKU的方式

数据库模型

  • 类目category
  • 品牌brand
  • 属性attribute
  • 标签tag
  • 商品主表/spu表/item表、item_stat 统计表、item属性值表
  • 商品变体表/variant表/sku表、sku attribute表
  • 其它实体表、其它实体和商品表的关联表


E-commerce product center

模型说明:

  • 商品(item/SPU)与商品变体(sku)分离,便于管理不同规格、价格、库存的商品。
  • 属性(attribute)、类目(category)、品牌(brand)等实体独立,便于扩展和维护
  • 商品分类体系如何设计?采用多级分类?分类的动态扩展只需插入新分类,指定其 parent_id,即可动态扩展任意层级
  • 灵活的属性体系。通过 category_attribute 和 spu_attr_value 支持不同类目下的不同属性,适应多样化商品需求。属性值与商品解耦,支持动态扩展
  • item_stat 单独存储统计信息,便于高并发下的读写优化。
  • 可以方便地增加标签(tag)、图片、描述、规格等字段,适应业务变化

商品信息录入JSON示例

实体商品
1、实体商品男士T恤 JSON 数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
{
"categoryId": 1003001,
"spu": {
"name": "经典圆领男士T恤",
"brandId": 2001,
"description": "柔软舒适,100%纯棉"
},
"basicAttributes": [
{
"attributeId": 101, // 品牌
"attributeName": "品牌",
"value": "NIKE"
},
{
"attributeId": 102, // 材质
"attributeName": "材质",
"value": "棉"
},
{
"attributeId": 103, // 产地
"attributeName": "产地",
"value": "中国"
},
{
"attributeId": 104, // 袖型
"attributeName": "袖型",
"value": "短袖"
}
],
"skus": [
{
"skuName": "黑色 L",
"price": 79.00,
"stock": 100,
"salesAttributes": [
{
"attributeId": 201,
"attributeName": "颜色",
"value": "黑色"
},
{
"attributeId": 202,
"attributeName": "尺码",
"value": "L"
}
]
},
{
"skuName": "白色 M",
"price": 79.00,
"stock": 150,
"salesAttributes": [
{
"attributeId": 201,
"attributeName": "颜色",
"value": "白色"
},
{
"attributeId": 202,
"attributeName": "尺码",
"value": "M"
}
]
}
]
}
虚拟商品
2、虚拟商品流量充值 JSON 数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
{
"categoryId": 1005002,
"spu": {
"name": "中国移动流量包充值",
"brandId": 3001,
"description": "全国通用流量包充值,按需选择,自动到账"
},
"basicAttributes": [
{
"attributeId": 301,
"attributeName": "运营商",
"value": "中国移动"
},
{
"attributeId": 302,
"attributeName": "适用网络",
"value": "4G/5G"
}
],
"skus": [
{
"skuName": "中国移动1GB全国流量包(7天)",
"price": 5.00,
"stock": 9999,
"salesAttributes": [
{
"attributeId": 401,
"attributeName": "流量容量",
"value": "1GB"
},
{
"attributeId": 402,
"attributeName": "有效期",
"value": "7天"
},
{
"attributeId": 403,
"attributeName": "流量类型",
"value": "全国通用"
}
]
},
{
"skuName": "中国移动5GB全国流量包(30天)",
"price": 20.00,
"stock": 9999,
"salesAttributes": [
{
"attributeId": 401,
"attributeName": "流量容量",
"value": "5GB"
},
{
"attributeId": 402,
"attributeName": "有效期",
"value": "30天"
},
{
"attributeId": 403,
"attributeName": "流量类型",
"value": "全国通用"
}
]
},
{
"skuName": "中国移动10GB全国流量包(90天)",
"price": 38.00,
"stock": 9999,
"salesAttributes": [
{
"attributeId": 401,
"attributeName": "流量容量",
"value": "10GB"
},
{
"attributeId": 402,
"attributeName": "有效期",
"value": "90天"
},
{
"attributeId": 403,
"attributeName": "流量类型",
"value": "全国通用"
}
]
}
]
}

商品的价格和库存

方案1. 价格和库存直接放在sku表中 (变化小)

在这种方案中,SKU(Stock Keeping Unit) 表包含商品的所有信息,包括价格和库存数量。每个 SKU 记录一个独立的商品实例,它有唯一的标识符,直接关联价格和库存。

1
2
3
4
5
6
7
8
9
10
11
12
CREATE TABLE sku_tab (
sku_id INT PRIMARY KEY, -- SKU ID
product_id INT, -- 商品ID (外键,指向商品表)
sku_name VARCHAR(255), -- SKU 名称
original_price DECIMAL(10, 2), -- 原始价格
price DECIMAL(10, 2), -- 销售价格
discount_price DECIMAL(10, 2), -- 折扣价格(如果有)
stock_quantity INT, -- 库存数量
warehouse_id INT, -- 仓库ID(如果有多个仓库)
created_at TIMESTAMP, -- 创建时间
updated_at TIMESTAMP -- 更新时间
);

优点:

  • 简单:所有信息都集中在一个表中,查询和管理都很方便。
  • 查询效率:查询某个商品的价格和库存不需要多表联接,减少了数据库查询的复杂度。
  • 维护方便:商品的所有信息(包括价格和库存)都在一个地方,减少了冗余数据和数据不一致的可能性。

缺点:

  • 灵活性差:如果价格和库存的管理策略较复杂(如促销、库存管理、动态定价等),这种方式可能不太适用。修改价格或库存时需要直接更新 SKU 表。
  • 扩展性差:对于一些复杂的定价和库存管理需求(如多层次的定价结构、分仓库管理等),直接放在 SKU 表中可能不够灵活。

适用场景:

  • 商品种类较少,SKU 数量相对固定且不复杂的场景。
  • 价格和库存变动较少,不涉及复杂的促销或动态定价的场景
方案2. 价格和库存单独管理(变化大)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

CREATE TABLE price_tab (
price_id INT PRIMARY KEY, -- 价格ID
sku_id INT, -- SKU ID (外键)
price DECIMAL(10, 2), -- 商品价格
discount_price DECIMAL(10, 2), -- 折扣价格
effective_date TIMESTAMP, -- 价格生效时间
expiry_date TIMESTAMP, -- 价格失效时间
price_type VARCHAR(50), -- 价格类型(如标准价、促销价等)
FOREIGN KEY (sku_id) REFERENCES ProductSKUs(sku_id)
);

CREATE TABLE inventory_tab (
inventory_id INT PRIMARY KEY, -- 库存ID
sku_id INT, -- SKU ID (外键)
quantity INT, -- 库存数量
warehouse_id INT, -- 仓库ID(如果有多个仓库)
updated_at TIMESTAMP, -- 库存更新时间
FOREIGN KEY (sku_id) REFERENCES ProductSKUs(sku_id)
);

优点:

  • 灵活性高:价格和库存信息可以独立管理,更容易支持多样化的定价策略、促销活动、库存管理等。
  • 可扩展性强:对于需要频繁更新价格、库存、促销等信息的商品,这种方案更容易扩展和适应变化。例如,可以灵活地增加新的价格策略或库存仓库。
  • 数据结构清晰:避免了价格和库存在 SKU 表中的冗余存储,使得数据结构更清晰。

缺点:

  • 查询复杂:获取某个商品的价格和库存信息时,需要联接多个表,查询效率可能会降低,尤其是在数据量大时。
  • 管理复杂:需要更多的表和关系,增加了维护成本和系统复杂度。

适用场景:

  • 商品种类繁多,SKU 数量较大,且需要支持动态定价、促销、库存管理等复杂需求的场景。
  • 需要频繁变动价格或库存的商品,且这些信息与 SKU 无法紧密绑定的场景

商品快照 item_snapshots

  1. 商品编辑时生成快照:
  • 每次商品信息(如价格、描述、属性等)发生编辑时,生成一个新的商品快照。
  • 将快照信息存储在 item_snapshots 表中,并生成一个唯一的 snapshot_id。
  1. 订单创建时使用快照:
    在用户下单时,查找当前商品的最新 snapshot_id。
    在 order_items 表中记录该 snapshot_id,以确保订单项反映下单时的商品状态
    1
    2
    3
    4
    5
    6
    7
    8
    9
    CREATE TABLE `snapshot_tab` (
    `snapshot_id` int(11) NOT NULL AUTO_INCREMENT,
    `snapshot_type` int(11) NOT NULL,
    `create_time` int(11) NOT NULL DEFAULT '0',
    `data` text NOT NULL,
    `entity_id` int(11) DEFAULT NULL,
    PRIMARY KEY (`snapshot_id`),
    KEY `idx_entity_id` (`entity_id`)
    )

用户操作日志

1
2
3
4
5
6
7
8
9
10
CREATE TABLE user_operation_logs (
log_id INT PRIMARY KEY AUTO_INCREMENT, -- Unique identifier for each log entry
user_id INT NOT NULL, -- ID of the user who made the edit
entity_id INT NOT NULL, -- ID of the entity being edited
entity_type VARCHAR(50) NOT NULL, -- Type of entity (e.g., SPU, SKU, Price, Stock)
operation_type VARCHAR(50) NOT NULL, -- Type of operation (e.g., CREATE, UPDATE, DELETE)
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- Time of the operation
details TEXT, -- Additional details about the operation
FOREIGN KEY (user_id) REFERENCES users(id) -- Assuming a users table exists
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

商品的统计信息

缓存的使用

核心流程

B端:商品创建和发布的流程

  • 批量上传、批量编辑
  • 单个上传、编辑
  • 审核、发布
  • OpenAPI,支持外部同步API push 商品
  • auto-sync,自动同步外部商品

C端:商品搜索、商品详情

  • 商品搜索
    • elastic search 索引构建。获取商品列表(首页索引)
    • 如何处理商品的SEO优化?
      1、item index
      
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      POST /products/_doc/1
      {
      "product_id": "123456",
      "name": "Wireless Bluetooth Headphones",
      "description": "High-quality wireless headphones with noise-cancellation.",
      "price": 99.99,
      "stock": 50,
      "category": "Electronics",
      "brand": "SoundMax",
      "sku": "SM-123",
      "spu": "SPU-456",
      "image_urls": [
      "http://example.com/images/headphones1.jpg",
      "http://example.com/images/headphones2.jpg"
      ],
      "ratings": 4.5,
      "seller_info": {
      "seller_id": "78910",
      "seller_name": "BestSeller"
      },
      "attributes": {
      "color": "Black",
      "size": "Standard",
      "material": "Plastic"
      },
      "release_date": "2023-01-15",
      "location": {
      "lat": 40.7128,
      "lon": -74.0060
      },
      "tags": ["headphones", "bluetooth", "wireless"],
      "promotional_info": "20% off for a limited time"
      }
  • 商品推荐
    • 商品的A/B测试如何设计?
    • 如何设计商品的推荐算法?
    • 商品的个性化定制如何实现?
  • 获取商品详情

订单管理 Order Center

订单系统,平台的”生命中轴线”

订单中需要包含哪些信息

常见的订单类型

  1. 实物订单
    典型场景:电商平台购物(如买衣服、家电)
    核心特征:
    需要物流配送,涉及收货地址、运费、物流跟踪
    需要库存校验与扣减
    售后流程(退货、换货、退款)复杂
    订单状态多(待发货、已发货、已收货等)

  2. 虚拟订单
    典型场景:会员卡、电子券、游戏点卡、电影票等
    核心特征:
    无物流配送,不需要收货地址和运费
    通常无需库存(或库存为虚拟库存)
    订单完成后直接发放虚拟物品或凭证
    售后流程简单或无售后

  3. 预售订单
    典型场景:新品预售、定金膨胀、众筹等
    核心特征:
    订单分为定金和尾款两阶段
    需校验定金支付、尾款支付的时效
    可能涉及定金不退、尾款未付订单自动关闭等规则
    发货时间通常在尾款支付后

  4. O2O订单,外卖订单
    典型场景:酒店预订
    核心特征:
    需选择入住/离店日期、房型、入住人信息
    需对接第三方酒店系统实时查房、锁房
    取消、变更政策复杂,可能涉及违约金
    无物流,但有电子凭证或入住确认

订单系统的演进

阶段 主要特征/能力 技术架构/数据模型 适用场景/目标 实现方式简单说明
初始阶段 - 实现订单基本流转(下单、支付、发货、收货、取消)
- 单一订单类型(实物订单)
- 订单与商品、用户简单关联
单体应用/单表或少量表结构 业务初期,订单量小,流程简单,SKU/商家数量有限 单体应用,单表存储,简单业务逻辑,直接数据库操作
成长阶段(订单中心) - 支持订单拆单、合单(如多仓发货、合并支付)
- 支持多品类订单(如实物+虚拟)
- 订单中心化,订单与支付、配送、售后等子系统解耦
- 订单与商品快照、操作日志关联
微服务/多表/订单中心架构 平台型电商,业务扩展,需支持多商家、多类型订单,订单量大幅增长 订单中心服务,微服务拆分,多表关联,服务间接口调用,快照与日志表设计
成熟期(平台化) - 支持多样化订单类型(预售、虚拟、O2O、定制、JIT等)
- 订单流程可配置/插件化/工作流引擎/状态机框架/规则引擎等
- 订单状态机、履约、支付、退款等子流程解耦
- 支持复杂的促销、分账、履约模式
分布式/服务化/灵活数据模型 大型/综合电商,业务复杂,需快速适应新业务模式和高并发场景 分布式服务,插件化/配置化流程,状态机驱动,异步消息,灵活数据模型
未来智能化 - 订单智能路由与分配(如智能分仓、智能客服)
- 实时风控与反欺诈
- 订单数据实时分析与洞察
- 高可用、弹性伸缩、自动化运维
云原生/大数据/AI驱动架构 超大规模平台,国际化、智能化、数据驱动,需极致稳定与创新能力 云原生架构,AI/大数据分析,自动化运维,弹性伸缩,智能路由与风控

常见的订单模型设计

订单表(order_tab):记录用户的购买订单信息。主键为 order_id。

  • pay_order_id:支付订单ID,作为外键关联支付订单。
  • user_id:用户ID,标识购买订单的用户。
  • total_amount:订单的总金额。
  • order_status:订单状态,如已完成、已取消等。
  • payment_status:支付状态,与支付订单相关。
  • fulfillment_status:履约状态,表示订单的配送或服务状态。
  • refund_status:退款状态,用于标识订单是否有退款

订单商品表(order_item_tab:记录订单中具体商品的信息。主键为 order_item_id。

  • order_id:订单ID,作为外键关联订单。
  • item_id:商品ID,表示订单中的商品。
  • item_snapshot_id:商品快照ID,记录当时购买时的商品信息快照。
  • item_status:商品状态,如已发货、退货等。
  • quantity:购买数量。
  • price:商品单价。
  • discount:商品折扣金额

订单支付表(pay_order_tab):主要用于记录用户的支付信息。主键为 pay_order_id,标识唯一的支付订单。

  • user_id:用户ID,标识支付的用户。
  • payment_method:支付方式,如信用卡、支付宝等。
  • payment_status:支付状态,如已支付、未支付等。
  • pay_amount、cash_amount、coin_amount、voucher_amount:支付金额、现金支付金额、代币支付金额、优惠券使用金额。
  • 时间戳字段包括创建时间、初始化时间和更新时间

退款表(refund_tab):记录订单或订单项的退款信息。主键为 refund_id。

  • order_id:订单ID,作为外键关联订单。
  • order_item_id:订单项ID,标识具体商品的退款。
  • refund_amount:退款金额。
  • reason:退款原因。
  • quantity:退款的商品数量。
  • refund_status:退款状态。
  • refund_time:退款操作时间。

实体间关系:

  • 支付订单与订单:
  • 一个支付订单可能关联多个购买订单,形成 一对多 关系。
    例如,用户可以通过一次支付购买多个不同的订单。
  • 订单与订单商品:
    一个订单可以包含多个订单项,形成 一对多 关系。
    订单项代表订单中所购买的每个商品的详细信息。
  • 订单与退款:
    • 一个订单可能包含多个退款,形成 一对多 关系。
    • 退款可以是针对订单整体,也可以针对订单中的某个商品

订单状态机设计

Order 主状态机

支付状态机

履约状态机

退货退款状体机

异常单人工介入

  • 用户发起退款单拒绝
  • 退货失败,订单状态无法流转
  • 退款失败
  • 退营销失败

订单ID 生成策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
  # 时间戳 + 机器id + uid % 1000 + 自增序号

import time
import threading
from typing import Union

class OrderNoGenerator:
def __init__(self, machine_id: int):
"""
初始化订单号生成器
:param machine_id: 机器ID (0-999)
"""
if not 0 <= machine_id <= 999:
raise ValueError("机器ID必须在0-999之间")

self.machine_id = machine_id
self.sequence = 0
self.last_timestamp = -1
self.lock = threading.Lock() # 线程锁,保证线程安全

def _wait_next_second(self, last_timestamp: int) -> int:
"""
等待下一秒
:param last_timestamp: 上次时间戳
:return: 新的时间戳
"""
timestamp = int(time.time())
while timestamp <= last_timestamp:
timestamp = int(time.time())
return timestamp

def generate_order_no(self, user_id: int) -> Union[int, str]:
"""
生成订单号
:param user_id: 用户ID
:return: 订单号(整数或字符串形式)
"""
with self.lock: # 使用线程锁保证线程安全
# 获取当前时间戳(秒级)
timestamp = int(time.time())

# 处理时间回拨
if timestamp < self.last_timestamp:
raise RuntimeError("系统时间回拨,拒绝生成订单号")

# 如果是同一秒,序列号自增
if timestamp == self.last_timestamp:
self.sequence = (self.sequence + 1) % 1000
# 如果序列号用完了,等待下一秒
if self.sequence == 0:
timestamp = self._wait_next_second(self.last_timestamp)
else:
# 不同秒,序列号重置
self.sequence = 0

self.last_timestamp = timestamp

# 获取用户ID的后3位
user_id_suffix = user_id % 1000

# 组装订单号
order_no = (timestamp * 1000000000 + # 时间戳左移9位
self.machine_id * 1000000 + # 机器ID左移6位
user_id_suffix * 1000 + # 用户ID左移3位
self.sequence) # 序列号

return order_no

def generate_order_no_str(self, user_id: int) -> str:
"""
生成字符串形式的订单号
:param user_id: 用户ID
:return: 字符串形式的订单号
"""
order_no = self.generate_order_no(user_id)
return f"{order_no:019d}" # 补零到19位

# 使用示例
def main():
# 创建订单号生成器实例
generator = OrderNoGenerator(machine_id=1)

# 生成订单号
user_id = 12345
order_no = generator.generate_order_no(user_id)
order_no_str = generator.generate_order_no_str(user_id)

print(f"整数形式订单号: {order_no}")
print(f"字符串形式订单号: {order_no_str}")

# 测试并发
def test_concurrent():
for _ in range(5):
order_no = generator.generate_order_no(user_id)
print(f"并发生成的订单号: {order_no}")

# 创建多个线程测试并发
threads = []
for _ in range(3):
t = threading.Thread(target=test_concurrent)
threads.append(t)
t.start()

# 等待所有线程完成
for t in threads:
t.join()

if __name__ == "__main__":
main()

订单商品快照

方案1. 直接使用商品系统的item snapshot。(由商品系统维护快照)

  • 商品系统负责维护商品快照
  • 订单系统通过引用商品快照ID来关联商品信息
  • 商品信息变更时,商品系统生成新的快照版本
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    -- 商品系统维护的快照表
    CREATE TABLE item_snapshot_tab (
    snapshot_id BIGINT PRIMARY KEY,
    item_id BIGINT NOT NULL,
    version INT NOT NULL,
    data JSON NOT NULL, -- 存储商品完整信息
    created_at TIMESTAMP NOT NULL,
    INDEX idx_item_version (item_id, version)
    );

    -- 订单系统引用快照
    CREATE TABLE order_item_tab (
    order_id BIGINT,
    item_id BIGINT,
    snapshot_id BIGINT, -- 引用商品快照
    quantity INT,
    price DECIMAL(10,2),
    FOREIGN KEY (snapshot_id) REFERENCES item_snapshot(snapshot_id)
    );
    优点
  • 数据一致性高:商品系统统一管理快照,避免数据不一致
  • 存储效率高:多个订单可以共享同一个快照版本
  • 维护成本低:订单系统不需要关心快照的生成和管理
  • 查询性能好:可以直接通过快照ID获取完整商品信息

缺点

  • 系统耦合度高:订单系统强依赖商品系统的快照服务
  • 扩展性受限:商品系统需要支持所有订单系统可能需要的商品信息
  • 版本管理复杂:需要处理快照的版本控制和清理
  • 跨系统调用:订单系统需要调用商品系统获取快照信息

方案2. 创单时提供商品详情信息。(由订单维护商品快照)

1
2
3
4
5
6
7
8
CREATE TABLE order_item (
order_id BIGINT,
item_id BIGINT,
quantity INT,
price DECIMAL(10,2),
snapshot_data JSON NOT NULL, -- 存储下单时的商品信息
FOREIGN KEY (order_id, item_id) REFERENCES order_item_snapshot(order_id, item_id)
);

方案3. 创单时提供商品详情信息。(由订单维护商品快照)+ 快照复用

设计思路:

  • 订单系统维护自己的快照表,但增加快照复用机制
  • 使用商品信息的摘要(摘要算法如MD5)作为快照的唯一标识
  • 相同摘要的商品信息共享同一个快照记录
  • 创单时先检查摘要是否存在,存在则复用,不存在则创建新快照
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-- 订单系统维护的快照表
CREATE TABLE order_item_snapshot (
snapshot_id BIGINT PRIMARY KEY,
item_id BIGINT NOT NULL,
item_hash VARCHAR(32) NOT NULL COMMENT '商品信息摘要',
snapshot_data JSON NOT NULL COMMENT '存储下单时的商品信息',
created_at TIMESTAMP NOT NULL,
INDEX idx_item_hash (item_hash),
INDEX idx_item_id (item_id)
);
-- 订单商品表
CREATE TABLE order_item (
order_id BIGINT,
item_id BIGINT,
snapshot_id BIGINT,
quantity INT,
price DECIMAL(10,2),
FOREIGN KEY (snapshot_id) REFERENCES order_item_snapshot(snapshot_id)
);

适用场景:

  • 商品模型比较固定,项目初期,团队比较小,能接受系统之间的耦合,可以考虑用1
  • 不同商品差异比较大,商品信息结构复杂,考虑用2
  • 订单量太大,考虑复用快照

核心流程

正常流程和逆向流程

创单

核心步骤
  1. 参数校验。用户校验,是否异常用户。
  2. 商品与价格校验。校验商品是否存在、是否上架、价格是否有效
  3. 库存校验与预占。检查库存是否充足,部分场景下进行库存预占(锁库存)。
  4. 营销信息校验。校验优惠券、积分等是否可用,计算优惠金额。
  5. 订单金额计算。计算订单总金额、应付金额、各项明细。
  6. 生成订单号。生成全局唯一订单号,保证幂等性。
  7. 订单数据落库。写入订单主表、订单明细表、扩展表等。
  8. 扣减库存、扣减实际库存(有的系统在支付后扣减)。
  9. 发送消息/异步处理。发送订单创建成功消息,通知库存、物流、营销等系统。
  10. 返回下单结果。返回订单号、支付信息等给前端。
实现思路
  • 接口定义:通过OrderCreationStep接口定义了每个步骤必须实现的方法
  • 上下文共享:使用OrderCreationContext在步骤间共享数据
  • 步骤独立:每个步骤都是独立的,便于维护和测试
  • 回滚机制:每个步骤都实现了回滚方法
  • 流程管理:通过OrderCreationManager统一管理步骤的执行和回滚
  • 错误处理:统一的错误处理和回滚机制
  • 可扩展性:易于添加新的步骤或修改现有步骤
  • 如何解决不同category 创单差异较大的问题?
    • 插件化/策略模式。将订单处理流程拆分为多个步骤(如校验、支付、通知等)。不同订单类型实现各自的处理逻辑,通过策略模式动态选择。
    1. 订单类型标识。在订单主表中增加订单类型字段,根据类型选择不同的处理流程。
    2. 扩展字段。使用JSON或扩展表存储特定订单类型的特殊字段(如酒店的入住日期、机票的航班信息)。
    3. 流程引擎。使用流程引擎(如BPMN)定义和管理复杂的订单处理流程,支持动态调整。
点击查看创单核心逻辑代码实现


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
package order

import (
"context"
"encoding/json"
"errors"
"fmt"
"log"
"time"
)

// OrderType 订单类型
type OrderType string

const (
OrderTypePhysical OrderType = "physical" // 实物订单
OrderTypeVirtual OrderType = "virtual" // 虚拟订单
OrderTypePresale OrderType = "presale" // 预售订单
OrderTypeHotel OrderType = "hotel" // 酒店订单
OrderTypeTopUp OrderType = "topup" // 充值订单
)

// OrderStatus 订单状态
type OrderStatus string

const (
OrderStatusInit OrderStatus = "init" // 初始化
OrderStatusPending OrderStatus = "pending" // 待支付
OrderStatusPaid OrderStatus = "paid" // 已支付
OrderStatusShipping OrderStatus = "shipping" // 发货中
OrderStatusSuccess OrderStatus = "success" // 成功
OrderStatusFailed OrderStatus = "failed" // 失败
OrderStatusCanceled OrderStatus = "canceled" // 已取消
)

// Order 订单基础信息
type Order struct {
ID string `json:"id"`
UserID string `json:"user_id"`
Type OrderType `json:"type"`
Status OrderStatus `json:"status"`
Amount float64 `json:"amount"`
Detail json.RawMessage `json:"detail"` // 不同类型订单的特殊字段
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}

// OrderCreationContext 创单上下文
type OrderCreationContext struct {
Ctx context.Context
Order *Order
Params map[string]interface{} // 创单参数
Cache map[string]interface{} // 步骤间共享数据
Errors []error // 错误记录
StepResults map[string]StepResult // 每个步骤的执行结果
RollbackFailedSteps []string // 记录回滚失败的步骤
}

// StepResult 步骤执行结果
type StepResult struct {
Success bool
Error error
Data interface{}
CompensateData interface{} // 用于补偿的数据
}

// OrderCreationStep 创单步骤接口
type OrderCreationStep interface {
Execute(ctx *OrderCreationContext) error
Rollback(ctx *OrderCreationContext) error
Compensate(ctx *OrderCreationContext) error // 异步补偿
Name() string
}

// 错误定义
var (
ErrInvalidParams = errors.New("invalid parameters")
ErrProductNotFound = errors.New("product not found")
ErrProductOffline = errors.New("product is offline")
ErrStockInsufficient = errors.New("stock insufficient")
ErrUserBlocked = errors.New("user is blocked")
ErrSystemBusy = errors.New("system is busy")
)

// OrderError 订单错误
type OrderError struct {
Step string
Message string
Err error
}

func (e *OrderError) Error() string {
return fmt.Sprintf("step: %s, message: %s, error: %v", e.Step, e.Message, e.Err)
}

// 参数校验步骤
type ParamValidationStep struct{}

func (s *ParamValidationStep) Execute(ctx *OrderCreationContext) error {
// 通用参数校验
if ctx.Order.UserID == "" || ctx.Order.Type == "" {
return &OrderError{Step: s.Name(), Message: "missing required fields", Err: ErrInvalidParams}
}

// 订单类型特殊参数校验
switch ctx.Order.Type {
case OrderTypePhysical:
if addr, ok := ctx.Params["address"].(string); !ok || addr == "" {
return &OrderError{Step: s.Name(), Message: "missing address for physical order", Err: ErrInvalidParams}
}
case OrderTypeHotel:
if _, ok := ctx.Params["check_in_date"].(time.Time); !ok {
return &OrderError{Step: s.Name(), Message: "missing check-in date for hotel order", Err: ErrInvalidParams}
}
}
return nil
}

func (s *ParamValidationStep) Rollback(ctx *OrderCreationContext) error {
// 参数校验步骤无需回滚
return nil
}

func (s *ParamValidationStep) Compensate(ctx *OrderCreationContext) error {
// 参数校验步骤无需补偿
return nil
}

func (s *ParamValidationStep) Name() string {
return "param_validation"
}

// Product 商品信息
type Product struct {
ID string
Name string
Price float64
IsOnSale bool
}

// ProductService 商品服务接口
type ProductService interface {
GetProduct(ctx context.Context, productID string) (*Product, error)
}

// StockService 库存服务接口
type StockService interface {
LockStock(ctx context.Context, productID string, quantity int) (string, error)
UnlockStock(ctx context.Context, lockID string) error
DeductStock(ctx context.Context, productID string, quantity int) error
RevertDeductStock(ctx context.Context, productID string, quantity int) error
}

// PromotionService 营销服务接口
type PromotionService interface {
ValidateCoupon(ctx context.Context, couponCode string, userID string, orderAmount float64) (*Coupon, error)
UseCoupon(ctx context.Context, couponCode string, userID string, orderID string) error
RevertCouponUsage(ctx context.Context, couponCode string, userID string, orderID string) error
DeductPoints(ctx context.Context, userID string, points int) error
RevertPointsDeduction(ctx context.Context, userID string, points int) error
}

// Coupon 优惠券信息
type Coupon struct {
Code string
Type string
Amount float64
Threshold float64
ExpireTime time.Time
}

// 商品校验步骤
type ProductValidationStep struct {
productService ProductService
}

func (s *ProductValidationStep) Execute(ctx *OrderCreationContext) error {
productID := ctx.Params["product_id"].(string)
product, err := s.productService.GetProduct(ctx.Ctx, productID)
if err != nil {
return &OrderError{Step: s.Name(), Message: "failed to get product", Err: err}
}

if !product.IsOnSale {
return &OrderError{Step: s.Name(), Message: "product is offline", Err: ErrProductOffline}
}

ctx.Cache["product"] = product
return nil
}

func (s *ProductValidationStep) Rollback(ctx *OrderCreationContext) error {
return nil
}

func (s *ProductValidationStep) Compensate(ctx *OrderCreationContext) error {
return nil
}

func (s *ProductValidationStep) Name() string {
return "product_validation"
}

// 库存校验步骤
type StockValidationStep struct {
stockService StockService
}

func (s *StockValidationStep) Execute(ctx *OrderCreationContext) error {
if ctx.Order.Type == OrderTypeVirtual || ctx.Order.Type == OrderTypeTopUp {
return nil
}

productID := ctx.Params["product_id"].(string)
quantity := ctx.Params["quantity"].(int)

lockID, err := s.stockService.LockStock(ctx.Ctx, productID, quantity)
if err != nil {
return &OrderError{Step: s.Name(), Message: "failed to lock stock", Err: err}
}

ctx.Cache["stock_lock_id"] = lockID
return nil
}

func (s *StockValidationStep) Rollback(ctx *OrderCreationContext) error {
if lockID, ok := ctx.Cache["stock_lock_id"].(string); ok {
return s.stockService.UnlockStock(ctx.Ctx, lockID)
}
return nil
}

func (s *StockValidationStep) Compensate(ctx *OrderCreationContext) error {
return nil
}

func (s *StockValidationStep) Name() string {
return "stock_validation"
}

// 库存扣减步骤
type StockDeductionStep struct {
stockService StockService
}

func (s *StockDeductionStep) Execute(ctx *OrderCreationContext) error {
// 虚拟商品和充值订单跳过库存扣减
if ctx.Order.Type == OrderTypeVirtual || ctx.Order.Type == OrderTypeTopUp {
return nil
}

productID := ctx.Params["product_id"].(string)
quantity := ctx.Params["quantity"].(int)

// 执行库存扣减
if err := s.stockService.DeductStock(ctx.Ctx, productID, quantity); err != nil {
return &OrderError{
Step: s.Name(),
Message: "failed to deduct stock",
Err: err,
}
}

// 记录扣减信息,用于回滚
ctx.Cache["stock_deducted"] = map[string]interface{}{
"product_id": productID,
"quantity": quantity,
}

return nil
}

func (s *StockDeductionStep) Rollback(ctx *OrderCreationContext) error {
deducted, ok := ctx.Cache["stock_deducted"].(map[string]interface{})
if !ok {
return nil
}

productID := deducted["product_id"].(string)
quantity := deducted["quantity"].(int)

return s.stockService.RevertDeductStock(ctx.Ctx, productID, quantity)
}

func (s *StockDeductionStep) Compensate(ctx *OrderCreationContext) error {
deducted, ok := ctx.Cache["stock_deducted"].(map[string]interface{})
if !ok {
return nil
}

productID := deducted["product_id"].(string)
quantity := deducted["quantity"].(int)

// 创建补偿消息
compensationMsg := StockCompensationMessage{
OrderID: ctx.Order.ID,
ProductID: productID,
Quantity: quantity,
Timestamp: time.Now(),
}

// TODO: 实现发送到补偿队列的逻辑
// return sendToCompensationQueue("stock_compensation", compensationMsg)
return nil
}

func (s *StockDeductionStep) Name() string {
return "stock_deduction"
}

// 营销活动扣减步骤
type PromotionDeductionStep struct {
promotionService PromotionService
}

func (s *PromotionDeductionStep) Execute(ctx *OrderCreationContext) error {
// 处理优惠券
if couponCode, ok := ctx.Params["coupon_code"].(string); ok {
// 验证优惠券
coupon, err := s.promotionService.ValidateCoupon(
ctx.Ctx,
couponCode,
ctx.Order.UserID,
ctx.Order.Amount,
)
if err != nil {
return &OrderError{
Step: s.Name(),
Message: "invalid coupon",
Err: err,
}
}

// 使用优惠券
if err := s.promotionService.UseCoupon(ctx.Ctx, couponCode, ctx.Order.UserID, ctx.Order.ID); err != nil {
return &OrderError{
Step: s.Name(),
Message: "failed to use coupon",
Err: err,
}
}

// 记录优惠券使用信息,用于回滚
ctx.Cache["used_coupon"] = couponCode

// 更新订单金额
ctx.Order.Amount -= coupon.Amount
}

// 处理积分抵扣
if points, ok := ctx.Params["use_points"].(int); ok && points > 0 {
// 扣减积分
if err := s.promotionService.DeductPoints(ctx.Ctx, ctx.Order.UserID, points); err != nil {
return &OrderError{
Step: s.Name(),
Message: "failed to deduct points",
Err: err,
}
}

// 记录积分扣减信息,用于回滚
ctx.Cache["deducted_points"] = points

// 更新订单金额(假设1积分=0.01元)
ctx.Order.Amount -= float64(points) * 0.01
}

return nil
}

func (s *PromotionDeductionStep) Rollback(ctx *OrderCreationContext) error {
// 回滚优惠券使用
if couponCode, ok := ctx.Cache["used_coupon"].(string); ok {
if err := s.promotionService.RevertCouponUsage(ctx.Ctx, couponCode, ctx.Order.UserID, ctx.Order.ID); err != nil {
return err
}
}

// 回滚积分扣减
if points, ok := ctx.Cache["deducted_points"].(int); ok {
if err := s.promotionService.RevertPointsDeduction(ctx.Ctx, ctx.Order.UserID, points); err != nil {
return err
}
}

return nil
}

func (s *PromotionDeductionStep) Compensate(ctx *OrderCreationContext) error {
// 优惠券补偿
if couponCode, ok := ctx.Cache["used_coupon"].(string); ok {
// TODO: 实现优惠券补偿逻辑
// 1. 发送到补偿队列
// 2. 记录补偿日志
// 3. 通知运营人员
}

// 积分补偿
if points, ok := ctx.Cache["deducted_points"].(int); ok {
// TODO: 实现积分补偿逻辑
// 1. 发送到补偿队列
// 2. 记录补偿日志
// 3. 通知运营人员
}

return nil
}

func (s *PromotionDeductionStep) Name() string {
return "promotion_deduction"
}

// OrderFactory 订单工厂
type OrderFactory struct {
commonSteps []OrderCreationStep
typeSteps map[OrderType][]OrderCreationStep
}

func NewOrderFactory() *OrderFactory {
f := &OrderFactory{
commonSteps: []OrderCreationStep{
&ParamValidationStep{},
&ProductValidationStep{},
&PromotionDeductionStep{},
},
typeSteps: make(map[OrderType][]OrderCreationStep),
}

// 实物订单特有步骤
f.typeSteps[OrderTypePhysical] = []OrderCreationStep{
&StockValidationStep{},
&StockDeductionStep{},
}

// 虚拟订单特有步骤
f.typeSteps[OrderTypeVirtual] = []OrderCreationStep{}

// 预售订单特有步骤
f.typeSteps[OrderTypePresale] = []OrderCreationStep{
&StockValidationStep{},
}

// 酒店订单特有步骤
f.typeSteps[OrderTypeHotel] = []OrderCreationStep{}

return f
}

func (f *OrderFactory) GetSteps(orderType OrderType) []OrderCreationStep {
steps := make([]OrderCreationStep, 0)
steps = append(steps, f.commonSteps...)
if typeSteps, ok := f.typeSteps[orderType]; ok {
steps = append(steps, typeSteps...)
}
return steps
}

// Logger 日志接口
type Logger interface {
Info(msg string, args ...interface{})
Error(msg string, args ...interface{})
}

// OrderCreationManager 订单创建管理器
type OrderCreationManager struct {
factory *OrderFactory
logger Logger
}

func (m *OrderCreationManager) CreateOrder(ctx context.Context, params map[string]interface{}) (*Order, error) {
orderCtx := &OrderCreationContext{
Ctx: ctx,
Params: params,
Cache: make(map[string]interface{}),
StepResults: make(map[string]StepResult),
RollbackFailedSteps: make([]string, 0),
}

// 初始化订单
order := &Order{
ID: generateOrderID(),
UserID: params["user_id"].(string),
Type: OrderType(params["type"].(string)),
Status: OrderStatusInit,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
orderCtx.Order = order

// 获取订单类型对应的处理步骤
steps := m.factory.GetSteps(order.Type)

// 执行步骤
executedSteps := make([]OrderCreationStep, 0)
for _, step := range steps {
stepName := step.Name()
m.logger.Info("executing step", "step", stepName)

err := step.Execute(orderCtx)
if err != nil {
m.logger.Error("step execution failed", "step", stepName, "error", err)

orderCtx.Errors = append(orderCtx.Errors, err)

// 执行回滚,并记录回滚失败的步骤
m.rollbackSteps(orderCtx, executedSteps)

// 只对回滚失败的步骤进行补偿
if len(orderCtx.RollbackFailedSteps) > 0 {
go m.compensateFailedRollbacks(orderCtx)
}

return nil, err
}

executedSteps = append(executedSteps, step)
m.logger.Info("step executed successfully", "step", stepName)
}

return order, nil
}

// 修改回滚逻辑,记录回滚失败的步骤
func (m *OrderCreationManager) rollbackSteps(ctx *OrderCreationContext, steps []OrderCreationStep) {
for i := len(steps) - 1; i >= 0; i-- {
step := steps[i]
stepName := step.Name()

if err := step.Rollback(ctx); err != nil {
m.logger.Error("step rollback failed", "step", stepName, "error", err)
// 记录回滚失败的步骤
ctx.RollbackFailedSteps = append(ctx.RollbackFailedSteps, stepName)
}
}
}

// 新的补偿方法,只处理回滚失败的步骤
func (m *OrderCreationManager) compensateFailedRollbacks(ctx *OrderCreationContext) {
m.logger.Info("starting compensation for failed rollbacks",
"failed_steps", ctx.RollbackFailedSteps)

// 获取所有步骤的映射
allSteps := make(map[string]OrderCreationStep)
for _, step := range m.factory.GetSteps(ctx.Order.Type) {
allSteps[step.Name()] = step
}

// 只对回滚失败的步骤进行补偿
for _, failedStepName := range ctx.RollbackFailedSteps {
if step, ok := allSteps[failedStepName]; ok {
if err := step.Compensate(ctx); err != nil {
m.logger.Error("step compensation failed",
"step", failedStepName,
"error", err)

// 补偿失败处理
m.handleCompensationFailure(ctx, failedStepName, err)
}
}
}
}

// 处理补偿失败的情况
func (m *OrderCreationManager) handleCompensationFailure(ctx *OrderCreationContext, stepName string, err error) {
// 创建补偿任务
compensationTask := CompensationTask{
OrderID: ctx.Order.ID,
StepName: stepName,
Params: ctx.Params,
Cache: ctx.Cache,
RetryCount: 0,
MaxRetries: 3,
CreatedAt: time.Now(),
}

// 记录错误日志
m.logger.Error("compensation task created for failed step",
"order_id", compensationTask.OrderID,
"step", compensationTask.StepName,
"error", err)

// TODO: 实现具体的补偿任务处理逻辑
// 1. 将任务保存到数据库
// 2. 发送到消息队列
// 3. 触发告警
}

// DefaultLogger 默认日志实现
type DefaultLogger struct{}

func NewDefaultLogger() Logger {
return &DefaultLogger{}
}

func (l *DefaultLogger) Info(msg string, args ...interface{}) {
log.Printf("INFO: "+msg, args...)
}

func (l *DefaultLogger) Error(msg string, args ...interface{}) {
log.Printf("ERROR: "+msg, args...)
}

// 辅助函数
func generateOrderID() string {
return fmt.Sprintf("ORDER_%d", time.Now().UnixNano())
}

// CompensationTask 补偿任务结构
type CompensationTask struct {
OrderID string
StepName string
Params map[string]interface{}
Cache map[string]interface{}
RetryCount int
MaxRetries int
CreatedAt time.Time
}

// StockCompensationMessage 库存补偿消息
type StockCompensationMessage struct {
OrderID string
ProductID string
Quantity int
Timestamp time.Time
}

支付

支付流程

1. 支付校验。用户校验,订单状态校验等 2. 营销活动扣减deduction、回滚rollback、补偿compensation. 3. 支付初始化 4. 支付回调 5. 补偿队列 6. OrderBus 订单事件
支付状态的设计
1
2
3
4
5
6
7
8
9
10
11
12
13
14
P0: PAYMENT_NOT_STARTED - 未开始
P1: PAYMENT_PENDING - 支付中,用户点击了pay按钮,等待支付)

P2: MARKETING_Init - 营销初始化
P3: MARKETING_FAILED - 营销扣减失败
P4: MARKETING_SUCCESS - 营销扣减成功

P5: PAYMENT_INITIALIZED - 支付初始化
P6: PAYMENT_INITIALIZED_FAILED - 支付初始化失败
P7: PAYMENT_PROCESSING - 支付处理中。(支付系统正在处理支付请求)
P8: PAYMENT_SUCCESS - 支付成功
P9: PAYMENT_FAILED - 支付失败
P10: PAYMENT_CANCELLED - 支付取消
P11: PAYMENT_TIMEOUT - 支付超时
异常和补偿设计

常见的异常:
营销部分:

  1. 营销扣减补偿操作重复。(营销接口幂等设计)
  2. 营销已经扣减了,但是后续步骤失败,需要回滚扣减的操作。(业务代码中需要有rollback操作)
  3. 营销已经扣减了,回滚扣减失败。延时队列任务补偿。(回滚失败发送延时队列,任务补偿)
  4. 营销已经扣减了,写延时队列失败,任务没有补偿成功。(补偿任务通过扫描异常单进行补偿)
  5. 营销已经扣减了,延时队列消息重复,重复回滚。(依赖营销系统的幂等操作)
  6. 营销已经扣减了,请求已经发给了营销服务,营销服务已经扣减了,但是回包失败。(请求营销接口之前更新订单状态为P2,针对P2的订单进行补偿)

支付部分:

  1. 重复支付。(支付接口幂等设计)
  2. 支付初始化请求支付成功,但是回包失败(重续针对P5的订单进行补偿,查询支付系统是否收单,已经支付结果查询)
  3. 支付回调包重复,更新回调结果幂等。
  4. 支付回调包丢失,对于P7支付单需要补偿。

履约

履约核心流程

履约状态机的设计
1
2
3
4
5
6
F0: FULFILLMENT_NOT_STARTED - 未开始
F1: FULFILLMENT_PENDING - 履约开始
F2: FULFILLMENT_PROCESSING - 履约处理中
F3: FULFILLMENT_FAILED - 履约失败
F4: FULFILLMENT_SUCCESS - 履约成功
F5: FULFILLMENT_CANCELLED - 履约取消
异常和补偿的设计
  1. 订阅支付完成的事件O2
  2. 在请求fulfillment/init履约初始化之前,更新订单状态为F1
  3. fulfillment/init 接口的回包丢了。(针对F1订单进行补偿)
  4. fulfillment/init 重复请求(幂等设计)
  5. F2订单补偿。(fulfillment/callback 丢包,处理失败等)

return & refund

主要流程
  1. 订单服务作为协调者。与履约服务、营销服务、支付服务解耦
  2. 用 OrderBus 进行事件传递
  3. 状体机设计
  4. 异常处理
异常和补偿机制
  1. 退货环节异常
  • 退货初始化失败:直接发送退款失败事件
  • 退货回调失败:更新状态为 R7,发送失败事件
  1. 营销退款异常
  • 营销处理失败:更新状态为 R14,发送失败事件
  • 营销处理成功:更新状态为 R13,继续后续流程
  1. 支付退款异常
  • 支付退款失败:更新状态为 R11,发送失败事件
  • 支付退款成功:更新状态为 R10,发送成功事件
订单详情查询

系统挑战和解决方案

如何维护订单状态的最终一致性?

不一致的原因

  • 重复请求
  • 丢包。例如,请求发货,对方收单,回包失败。
  • 资源回滚:营销、库存
  • 并发问题

状态机

  • 设计层面,严格的状态转换规则 + 状态转换的触发事件
  • 状态转换的原子性。(事务性)

并发更新数据库前,要用乐观锁或者悲观锁,

  • 乐观锁:同时在更新时判断版本号是否是之前取出来的版本号,更新成功就结束
  • 悲观锁:先使用select for update进行锁行记录,然后更新
1
2
3
4
5
6
7
8
9
UPDATE orders 
SET status = 'NEW_STATUS',
version = version + 1
WHERE id = ? AND version = ?

BEGIN;
SELECT * FROM orders WHERE id = ? FOR UPDATE;
UPDATE orders SET status = 'NEW_STATUS' WHERE id = ?;
COMMIT;

幂等设计。比如重复支付、重复扣减营销、重复履约等

  • 支付重复支付,支付回调幂等设计。
  • 重复营销扣减,回滚,
  • 重复履约
  • 重复回调
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 使用支付单号作为幂等键
@Transactional
public void handlePaymentCallback(String paymentId, String status) {
// 检查是否已处理
if (isProcessed(paymentId)) {
return;
}
// 处理支付回调
processPaymentCallback(paymentId, status);
// 记录处理状态
markAsProcessed(paymentId);
}


// 使用订单号+营销资源ID作为幂等键
@Transactional
public void deductMarketingResource(String orderId, String resourceId) {
if (isDeducted(orderId, resourceId)) {
return;
}
// 扣减营销资源
deductResource(orderId, resourceId);
// 记录扣减状态
markAsDeducted(orderId, resourceId);
}

补偿机制兜底

  • 异常回滚。营销扣减回滚
  • 消息队列补偿:补偿队列,重试。(可能丢消息)
  • 定时任务补偿:扫表补偿
  • 依赖方支付查询和幂等设计

分布式事务

  • 营销扣减
  • 库存扣减
  • 支付等业务
  • 实现状态转换和业务操作在同一个事务中完成
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    @Transactional
    public void processOrderWithDistributedTransaction(Order order) {
    try {
    // 1. 更新订单状态
    updateOrderStatus(order);
    // 2. 扣减库存
    deductInventory(order);
    // 3. 创建物流单
    createLogistics(order);
    } catch (Exception e) {
    // 触发补偿机制
    triggerCompensation(order);
    }
    }

异常单人工介入

对账机制

商品信息缓存和数据一致性

主从架构中如何获取最新的数据,避免因为主从延时导致获得脏数据

策略 优点 缺点
1. 直接读取主库 - 一致性: 始终获取最新的数据。 - 性能: 增加主库的负载,可能导致性能瓶颈。
- 简单性: 实现简单直接,因为它直接查询可信的源。 - 可扩展性: 主库可能成为瓶颈,限制系统在高读流量下有效扩展的能力。
2. 使用VersionCache与从库 - 性能: 分散读取负载到从库,减少主库的压力。 - 复杂性: 实现更加复杂,需要进行缓存管理并处理潜在的不一致性问题。
- 可扩展性: 通过将大部分读取操作卸载到从库,实现更好的扩展性。 - 缓存管理: 需要进行适当的缓存失效处理和同步,以确保数据的一致性。
- 一致性: 通过比较版本并在必要时回退到主库,提供确保最新数据的机制。 - 潜在延迟: 从库的数据可能仍然存在不同步的可能性,导致数据更新前有轻微延迟。

常见问题1: 重复下单、支付、履约问题(重复和幂等问题)

场景:

  1. 下单、去重、DB唯一键兜底。去重逻辑是约定的
  2. 支付、checkoutid,唯一键
  3. 履约、先获取reference id,再履约

解决方案:

  1. 前端方案
    前端通过js脚本控制,无法解决用户刷新提交的请求。另外也无法解决恶意提交。
    不建议采用该方案,如果想用,也只是作为一个补充方案。

  2. 中间环节去重。根据请求参数中间去重
    当用户点击购买按钮时,渲染下单页面,展示商品、收货地址、运费、价格等信息,同时页面会埋上 Token 信息,用户提交订单时,后端业务逻辑会校验token,有且匹配才认为是合理请求。

  3. 利用数据库自身特性 “主键唯一约束”,在插入订单记录时,带上主键值,如果订单重复,记录插入会失败。
    操作过程如下:
    引入一个服务,用于生成一个”全局唯一的订单号”;
    进入创建订单页面时,前端请求该服务,预生成订单ID;
    提交订单时,请求参数除了业务参数外,还要带上这个预生成订单ID

快照和操作日志

为了保证数据的 完整性、可追溯性,写操作需要关注的问题
场景:
商品信息是可以修改的,当用户下单后,为了更好解决后面可能存在的买卖纠纷,创建订单时会同步保存一份商品详情信息,称之为订单快照

解决方案:
同一件商品,会有很多用户会购买,如果热销商品,短时间就会有上万的订单。如果每个订单都创建一份快照,存储成本太高。另外商品信息虽然支持修改,但毕竟是一个低频动作。我们可以理解成,大部分订单的商品快照信息都是一样的,除非下单时用户修改过。
如何实时识别修改动作是解决快照成本的关键所在。我们采用摘要比对的方法‍。创建订单时,先检查商品信息摘要是否已经存在,如果不存在,会创建快照记录。订单明细会关联商品的快照主键。

账户余额更新,保证事务
用户支付,我们要从买家账户减掉一定金额,再往卖家增加一定金额,为了保证数据的 完整性、可追溯性, 变更余额时,我们通常会同时插入一条 记录流水。

账户流水核心字段: 流水ID、金额、交易双方账户、交易时间戳、订单号。
账户流水只能新增,不能修改和删除。流水号必须是自增的。
后续,系统对账时,我们只需要对交易流水明细数据做累计即可,如果出现和余额不一致情况,一般以交易流水为准来修复余额数据。
更新余额、记录流水 虽属于两个操作,但是要保证要么都成功,要么都失败。要做到事务。
当然,如果涉及多个微服务调用,会用到 分布式事务。
分布式事务,细想下也很容易理解,就是 将一个大事务拆分为多个本地事务, 本地事务依然借助于数据库自身事务来解决,难点在于解决这个分布式一致性问题,借助重试机制,保证最终一致是我们常用的方案。

常见问题3: 并发更新的ABA问题 (订单表的version)

场景:
商家发货,填写运单号,开始填了 123,后来发现填错了,然后又修改为 456。此时,如果就为某种特殊场景埋下错误伏笔,具体我们来看下,过程如下:
开始「请求A」发货,调订单服务接口,更新运单号 123,但是响应有点慢,超时了;
此时,商家发现运单号填错了,发起了「请求B」,更新运单号为 456 ,订单服务也响应成功了;
这时,「请求A」触发了重试,再次调用订单服务,更新运单号 123,订单服务也响应成功了;订单服务最后保存的 运单号 是 123。

是不是犯错了!!!!,那么有什么好的解决方案吗?
数据库表引入一个额外字段 version ,每次更新时,判断表中的版本号与请求参数携带的版本号是否一致。这个版本字段可以是时间戳
复制
update order
set logistics_num = #{logistics_num} , version = #{version} + 1
where order_id= 1111 and version = #{version}

秒杀系统中的库存管理和订单蓄洪

常见的库存扣减方式有:
下单减库存: 即当买家下单后,在商品的总库存中减去买家购买数量。下单减库存是最简单的减库存方式,也是控制最精确的一种,但是有些人下完单可能并不会付款。
付款减库存: 即买家下单后,并不立即减库存,而是等到有用户付款后才真正减库存,否则库存一直保留给其他买家。但因为付款时才减库存,如果并发比较高,有可能出现买家下单后付不了款的情况,因为可能商品已经被其他人买走了。
预扣库存: 这种方式相对复杂一些,买家下单后,库存为其保留一定的时间(如 30 分钟),超过这个时间,库存将会自动释放,释放后其他买家就可以继续购买。在买家付款前,系统会校验该订单的库存是否还有保留:如果没有保留,则再次尝试预扣;
方案一:数据库乐观锁扣减库存
通常在扣减库存的场景下使用行级锁,通过数据库引擎本身对记录加锁的控制,保证数据库的更新的安全性,并且通过where语句的条件,保证库存不会被减到 0 以下,也就是能够有效的控制超卖的场景。
先查库存
然后乐观锁更新:update … set amount = amount - 1 where id = $id and amount = x
设置数据库的字段数据为无符号整数,这样减后库存字段值小于零时 SQL 语句会报错
方案二:redis 扣减库存,异步同步到DB
redis 原子操作扣减库存
异步通过MQ消息同步到DB

购物车模块的实现和优化

技术设计并不是特别复杂,存储的信息也相对有限(用户id、商品id、sku_id、数量、添加时间)。这里特别拿出来单讲主要是用户体验层面要注意几个问题:
添加购物车时,后端校验用户未登录,常规思路,引导用户跳转登录页,待登录成功后,再添加购物车。多了一步操作,给用户一种强迫的感觉,体验会比较差。有没有更好的方式?
如果细心体验京东、淘宝等大平台,你会发现即使未登录态也可以添加购物车,这到底是怎么实现的?
细细琢磨其实原理并不复杂,服务端这边在用户登录态校验时,做了分支路由,当用户未登录时,会创建一个临时Token,作为用户的唯一标识,购物车数据挂载在该Token下,为了避免购物车数据相互影响以及设计的复杂度,这里会有一个临时购物车表。
当然,临时购物车表的数据量并不会太大,why?用户不会一直闲着添加购物车玩,当用户登录后,查看自己的购物车,服务端会从请求的cookie里查找购物车Token标识,并查询临时购物车表是否有数据,然后合并到正式购物车表里。
临时购物车是不是一定要在服务端存储?未必。
有架构师倾向前置存储,将数据存储在浏览器或者 APP LocalStorage, 这部分数据毕竟不是共享的,但是不太好的增加了设计的复杂度。

客户端需要借助本地数据索引,远程请求查完整信息;
如果是登录态,还要增加数据合并逻辑;
考虑到这两部分数据只是用户标识的差异性,所以作者还是建议统一存到服务端,日后即使业务逻辑变更,只需要改一处就可以了,毕竟自运营系统,良好的可维护性也需要我们非常关注的。

购物车是电商系统的标配功能,暂存用户想要购买的商品。

  • 分为添加商品、列表查看、结算下单三个动作。
  • 用户未登录时,将数据存储在浏览器或者 APP LocalStorage。登录后写入后端
  • 后端使用DB,为了性能考虑可以结合redis和DB联合存储
  • 存redis定期同步到DB
  • 前后端联合存储

系统中的分布式ID是怎么生成的

item ID 自增。(100w级别)
order id. 时间戳 + 机器ID + uid % 100 + sequence
DP唯一ID生成调研说明
request 生成方法:时间戳 + 机器mac地址 + sequence

系统稳定性建设

Google 理论:怎样的系统算是稳定高可用的

Google SRE中(SRE三部曲[1])有一个层级模型来描述系统可靠性基础和高层次需求(Dickerson’s Hierarchy of Service Reliability),如下图:


该模型由Google SRE工程师Mikey Dickerson在2013年提出,将系统稳定性需求按照基础程度进行了不同层次的体系化区分,形成稳定性标准金字塔模型:

  1. 金字塔的底座是监控(Monitoring),这是一个系统对于稳定性最基础的要求,缺少监控的系统,如同蒙上眼睛狂奔的野马,无从谈及可控性,更遑论稳定性。

  2. 更上层是应急响应(Incident Response),从一个问题被监控发现到最终解决,这期间的耗时直接取决于应急响应机制的成熟度。合理的应急策略能保证当故障发生时,所有问题能得到有序且妥善的处理,而不是慌乱成一锅粥。

  3. 研发流程规范

  • 测试和发布管控(Testing&Release procedures),大大小小的应用都离不开不断的变更与发布,有效的测试与发布策略能保障系统所有新增变量都处于可控稳定区间内,从而达到整体服务终态稳定
  • 事后总结以及根因分析(Postmortem&Root Caue Analysis),即我们平时谈到的”复盘”,虽然很多人都不太喜欢这项活动,但是不得不承认这是避免我们下次犯同样错误的最有效手段,只有当摸清故障的根因以及对应的缺陷,我们才能对症下药,合理进行规避
  1. 容量规划(Capacity Planning)则是针对于这方面变化进行的保障策略。现有系统体量是否足够支撑新的流量需求,整体链路上是否存在不对等的薄弱节点,都是容量规划需要考虑的问题。

  2. 高可用性产品设计(Product)与软件研发(Development),即通过优秀的产品设计与软件设计使系统具备更高的可靠性,构建高可用产品架构体系,从而提升用户体验

  3. 除此之外,还需要系统资损防控等

下面也会从六个个方面分别介绍


可观测性设计

监控指标 - 完备性


监控类型 监控指标
业务监控 - 订单量、GMV、转化率等业务KPI
- 用户活跃度、留存率等用户指标
- 支付成功率、退款率等交易指标
- 下单失败率
- 支付超时率
- 库存不足率
应用监控 - 接口响应时间(RT)
- 接口调用量(QPS)
- 接口成功率
- 应用JVM指标
- 线程池使用情况
- 应用日志监控
系统监控 - 服务器CPU/内存/磁盘使用率
- 网络带宽使用率
- 系统负载
- 容器资源使用情况
- 网关流量/延迟/错误率
外部依赖 - RPC调用成功率/延迟
- 数据库连接池状态/慢查询
- 缓存命中率/延迟
- 消息队列积压量/消费延迟
- 第三方服务调用成功率/超时率
- 分布式锁获取成功率
- 分布式事务状态
- CDN服务质量
- 对象存储可用性
其它 - 安全相关指标(登录失败/异常IP等)
- 业务告警统计/处理率
- 系统变更记录/回滚率
- 运维操作审计
- 数据备份状态
- 证书过期监控
- 配置变更记录
- 资源成本监控
- 服务SLA达标率

告警 - 实时性和有效性

是不是每项监控都需要告警?答案当然是否定的。建议优先设置Biz层告警,因为Biz层我们对外服务最直观业务表现,最贴切用户感受。Application&System层指标主要用于监控,部分关键&高风险指标可设置告警,用于问题排查定位以及故障提前发现。对于一项告警,我们一般需要关注级别、阈值、通知人等几个点。

  1. 级别
    即当前告警被触发时,问题的严重程度,一般来说有几个衡量点:
  • 是否关联NOC.影响业务指标,事故级别
  • 是否影响核心链路,应用指标异常
  • 是否产生资损
  1. 阈值
  • 即一项告警的触发条件&时间,需根据具体场景合理制定。一般遵循以下原则:
  • 不可过于迟钝。一个合理的监控体系中,任何异常发生后都应触发相关告警。
  • 不可过于敏感。过于敏感的阈值会造成频繁告警,从而导致响应人员疲劳应对,无法筛选真实异常。若一个告警频繁出现,一般是两个原因:系统设计不合理 or 阈值设置不合理。
  • 若单一指标无法反馈覆盖整体业务场景,可结合多项指标关联构建。
  • 需符合业务波动曲线,不同时段可设置不同条件&通知策略。
  • 定期review 告警指标合理性
  1. 通知人&方式
  • 若为业务指标异常(Biz层告警),通知人应为问题处理人员(开发、运维同学)与业务关注人员(TL、业务同学)的集合,通知方式较为实时,比如电话通知。
  • 若为应用 & 系统层告警,主要用于定位异常原因,通知人设置问题排查处理人员即可,通知方式可考虑钉钉、短信等低干扰方式。
  • 除了关联层次,对于不同级别的告警,通知人范围也可适当扩大,尤其是关联GOC故障的告警指标,应适当放宽范围,通知方式也应更为实时直接

日志体系

  • 日志收集
  • 日志分析
  • 日志存储
  • 日志检索

追踪体系

  • 分布式追踪
  • 性能分析
  • 异常追踪
  • 依赖分析

异常应急策略

常用的应急策略

  • 降级策略
  • 限流策略
  • 熔断策略
  • 超时配置化(context 超时配置和传递)
  • 补偿策略
  • 其它业务策略:
    • 商品暂停销售
    • 营销券暂停发送等

降级策略

策略类型 具体场景 实现方式 应用案例 配置建议
降级策略 服务降级 降级开关 推荐服务返回本地缓存 开关粒度: 接口级
降级时长: 5min
功能降级 业务降级 搜索服务简化召回逻辑 降级优先级配置
降级比例配置

限流策略

策略类型 具体场景 实现方式 应用案例 配置建议
限流策略 单机接口限流(粗略) 令牌桶/漏桶算法 下单接口限制QPS为1000 QPS: 1000
突发流量: 1200
分布式限流(全局限流) Redis + Lua脚本 秒杀接口全局限限流 \n 用户维度限流 全局QPS: 5000
单机QPS: 1000,时间窗口: 1min
最大请求数: 5

熔断策略

策略类型 具体场景 实现方式 应用案例 配置建议
熔断策略 服务调用熔断 Hystrix-go 外部支付服务调用保护 错误阈值: 50%
最小请求数: 20
超时时间: 1s
中间件数据库、缓存熔断 Circuit Breaker 数据库访问保护,缓存访问保护 错误阈值: 30%
恢复时间: 5s

补偿策略

策略类型 具体场景 实现方式 应用案例 配置建议
补偿机制 状态机补偿 定时任务 订单状态异常修复 执行间隔: 5min
重试次数: 3
数据补偿 人工触发 库存数据不一致修复 补偿任务可回滚
补偿日志记录
业务补偿 消息队列 支付结果异步通知 重试策略配置
补偿通知方式
降级补偿 降级恢复 服务恢复后数据同步 补偿优先级
补偿超时时间

组合策略

组合策略应用场景

业务场景 组合策略 实现方式 配置建议
秒杀系统 限流 + 熔断 + 降级 1. 入口限流
2. 服务熔断
3. 降级返回
- 限流: 5000 QPS
- 熔断: 50% 错误率
- 降级: 返回售罄
订单系统 限流 + 补偿 + 熔断 1. 用户限流
2. 状态补偿
3. DB熔断
- 用户限流: 5次/分钟
- 补偿间隔: 1分钟
- DB超时: 1秒
支付系统 熔断 + 降级 + 补偿 1. 支付熔断
2. 降级支付
3. 异步补偿
- 熔断阈值: 30%
- 降级通道: 备用
- 补偿周期: 实时
库存系统 限流 + 熔断 + 补偿 1. 接口限流
2. 缓存熔断
3. 数据补偿
- QPS限制: 1000
- 熔断恢复: 5s
- 补偿策略: 定时

其它业务策略:

  1. 商品暂停销售
  2. 营销券暂停发送等

业务和容量规划

业务策略

不同于高可用系统建设体系,大促稳定性保障体系与面向特定业务活动的针对性保障建设,因此,业务策略与数据是我们进行保障前不可或缺的数据。
一般大促业务数据可分为两类,全局业务形态评估以及应急策略&玩法。

流量模型评估

  1. 入口流量
    对于一次大促,系统峰值入口流量一般由常规业务流量与非常规增量(比如容灾预案&业务营销策略变化带来的流量模型配比变化)叠加拟合而成。
  • 常规业务流量一般有两类计算方式:
    • 历史流量算法:该类算法假设当年大促增幅完全符合历史流量模型,根据当前&历年日常流量,计算整体业务体量同比增量模型;然后根据历年大促-日常对比,计算预估流量环比增量模型;最后二者拟合得到最终评估数据。
    • 由于计算时无需依赖任何业务信息输入,该类算法可用于保障工作初期业务尚未给出业务总量评估时使用,得到初估业务流量。
    • 业务量-流量转化算法(GMV\DAU\订单量):该类算法一般以业务预估总量(GMV\DAU\订单量)为输入,根据历史大促&日常业务量-流量转化模型(比如经典漏洞模型)换算得到对应子域业务体量评估。- 该种方式强依赖业务总量预估,可在保障工作中后期使用,在初估业务流量基础上纳入业务评估因素考虑。
  • 非常规增量一般指前台业务营销策略变更或系统应急预案执行后流量模型变化造成的增量流量。例如,NA61机房故障时,流量100%切换到NA62后,带来的增量变化.考虑到成本最小化,非常规增量P计算时一般无需与常规业务流量W一起,全量纳入叠加入口流量K,一般会将非常规策略发生概率λ作为权重
  1. 节点流量
    节点流量由入口流量根据流量分支模型,按比例转化而来。分支流量模型以系统链路为计算基础,遵循以下原则:
  • 同一入口,不同链路占比流量独立计算。
  • 针对同一链路上同一节点,若存在多次调用,需计算按倍数同比放大(比如DB\Tair等)。
  • DB写流量重点关注,可能出现热点造成DB HANG死。

容量转化

节点容量是指一个节点在运行过程中,能够同时处理的最大请求数。它反映了系统的瞬时负载能力。

1)Little Law衍生法则
不同类型资源节点(应用容器、Tair、DB、HBASE等)流量-容量转化比各不相同,但都服从Little Law衍生法则,即:
节点容量=节点吞吐率×平均响应时间
2)N + X 冗余原则

在满足目标流量所需要的最小容量基础上,冗余保留X单位冗余能力
X与目标成本与资源节点故障概率成正相关,不可用概率越高,X越高
对于一般应用容器集群,可考虑X = 0.2N

全链路压测

成本优化

研发流程规范

  • 研发
  • 测试
  • 发布

高可用的架构设计

整体架构

架构层面 设计类型 具体措施 说明
整体架构和部署 冗余设计 多机房部署 在不同地理位置部署多个机房,避免单点故障
服务多副本 每个服务部署多个实例,提供故障转移能力
数据多副本 数据多副本存储,保证数据可靠性
无状态服务设计 服务无状态化,便于水平扩展
容灾设计 同城双活 同一城市两个机房同时对外服务
异地多活 多地机房同时对外服务,就近访问
机房级容灾 单机房故障时可切换到其他机房
数据中心容灾 数据中心级别的容灾能力
灾备演练 演练计划 定期制定灾备演练计划
演练流程 标准化的演练流程和checklist
效果评估 对演练结果进行评估和复盘
持续改进 根据演练反馈持续优化容灾方案
安全架构 网络安全 防火墙/WAF 防御网络攻击,过滤恶意流量
VPN/专线 安全的网络连接方式
数据安全 加密存储 敏感数据加密存储
访问控制 严格的数据访问权限控制
应用安全 身份认证 多因素认证,防止账号盗用
操作审计 记录关键操作审计日志

应用层架构

设计类型 具体措施 说明
隔离设计 服务隔离 核心服务独立部署,避免互相影响
数据隔离 按业务领域划分数据存储
故障隔离 故障隔离机制,防止故障扩散
资源隔离 CPU/内存等资源隔离管理
水平扩展设计 服务无状态化 服务设计无状态,便于扩容
数据分片策略 数据分片存储,支持横向扩展
弹性伸缩 根据负载自动扩缩容
负载均衡 多实例间的负载分配策略

系统链路梳理和维护 System & Biz Profiling

系统链路梳理是所有保障工作的基础,它就像对整个应用系统进行一次全面体检。从流量入口开始,按照链路轨迹,逐级分层节点,最终得到系统全局画像与核心保障点。主要包含以下几个方面:

  1. 入口梳理盘点
    系统通常有多个流量入口,包括HTTP、RPC、消息等多种来源。建议优先从以下三类重点入口开始梳理:
  • 核心重保流量入口
    • 有明确的服务SLI承诺,对数据准确性、响应时间、可靠度有严格要求
    • 业务核心链路(浏览、下单、支付、履约等)
    • 面向企业级用户的服务
  • 资损风险入口
    • 涉及公司或客户资金收入的收费服务
    • 可能造成资金损失的关键节点
  • 大流量入口
    • 系统TPS/QPS排名前5-10的入口
    • 虽无高SLI要求但流量大,对系统整体负载影响显著
  1. 节点分层分析
    按照流量轨迹,对链路上的各类节点(HSF、DB、Tair、HBase等外部依赖)进行分层分析:
  • 强弱依赖判定

    • 业务强依赖:节点不可用会导致业务中断或严重受损
    • 系统强依赖:节点不可用会导致系统执行中断
    • 性能强依赖:节点不可用会严重影响系统性能
    • 弱依赖:有降级方案或影响较小
  • 可用性风险判定

    • 日常超时频发的节点
    • 系统资源紧张的节点
    • 历史故障频发的节点
  • 高风险节点识别

    • 近期改造过的节点
    • 首次参与大促的新节点
    • 曾出现过高级别故障的节点
    • 可能造成资损的关键节点
  1. 调用关系分析
  • 绘制核心接口调用拓扑图或时序图
  • 计算各节点间调用比例
  • 分析调用链路上的资损风险点
  • 识别内外部系统依赖关系
  1. 系统调用拓扑图(可借助分布式链路追踪系统),包含QPS、强弱依赖、高风险节点标注

  2. 接口时序图(以创建订单为例)

资损防控架构

定期review资损风险
事中及时发现


【得物技术】浅谈资损防控

事后复盘和知识沉淀

性能优化


  1. 应用层优化

    • 代码优化
    • JVM调优
    • 线程池优化
    • 连接池优化
  2. 数据层优化

    • SQL优化
    • 索引优化
    • 分库分表
    • 读写分离
  3. 缓存优化

    • 多级缓存
    • 热点缓存
    • 缓存预热
    • 缓存穿透/击穿防护

其它常见问题

基础概念与架构设计

  • 电商后台系统的核心架构设计原则有哪些?
  • 电商后台系统与前端系统的交互方式有哪些?各自的特点是什么?
  • 如何设计电商后台系统的用户权限管理模块?
  • 电商后台系统中,微服务架构和单体架构的适用场景分别是什么?
  • 简述电商后台系统的分层架构设计,各层的主要职责是什么?
  • 如何实现电商后台系统的接口幂等性?
  • 电商后台系统中,分布式 Session 管理有哪些常见方案?
  • 设计电商后台系统时,如何考虑系统的可扩展性和可维护性?
  • 电商后台系统的 API 设计规范应包含哪些内容?
  • 如何设计电商后台系统的异常处理机制?

商品管理

  • 什么是SPU和SKU?它们之间的关系是什么?

  • 电商系统中的商品分类体系是如何设计的? category 父子类目

  • 什么是商品属性?如何区分规格属性(Sales Attributes))和非规格属性?属性,会影响商品SKU的属性直接关系到库存和价格,用户购买时需要选择的属性,例如:颜色、尺码、内存容量等。非规格属性(Basic Attributes):用于描述商品特征,产地、材质、生产日期

  • 商品的生命周期包含哪些状态?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    创建阶段
    DRAFT(0): 草稿状态
    PENDING_AUDIT(1): 待审核
    AUDIT_REJECTED(2): 审核拒绝
    AUDIT_APPROVED(3): 审核通过
    销售阶段
    ON_SHELF(10): 在售/上架
    OFF_SHELF(11): 下架
    SOLD_OUT(12): 售罄
    特殊状态
    FROZEN(20): 冻结(违规/投诉)
    DELETED(99): 删除

    什么是商品快照?为什么需要商品快照?

  • 商品的 SKU 和 SPU 概念在后台系统中如何体现?两者的关系是怎样的?

  • 电商后台系统中商品的基础信息包括哪些?如何设计商品表的数据库模型?

  • 商品的库存管理和价格管理在后台系统中是如何关联的?

  • 如何处理商品的多规格(如颜色、尺寸、型号等)信息?数据库表结构如何设计?

  • 商品详情页的信息(如描述、图片、参数)在后台系统中如何存储和管理?

  • 商品上下架的逻辑在后台系统中是如何实现的?需要考虑哪些因素(如库存、审核状态等)?

  • 商品的搜索和筛选功能在后台系统中是如何实现的?涉及哪些技术(如全文搜索、数据库索引等)?

  • 新品发布和商品淘汰在后台系统中的处理流程是怎样的?

  • 如何保证商品信息的唯一性和完整性,避免重复录入和数据错误?

  1. 唯一性保证:
    使用唯一索引
    引入商品编码系统
    查重机制
  2. 完整性保证:
    必填字段验证
    数据格式校验
    关联完整性检查
    业务规则校验

订单管理

  • 电商后台系统中订单的主要状态有哪些?状态流转的触发条件和处理逻辑是怎样的?
  • 订单的创建流程在后台系统中是如何处理的?涉及哪些模块(如库存、价格、用户信息等)的交互?
  • 如何实现订单的分单处理(如不同仓库发货、不同店铺订单拆分)?
  • 订单的支付状态如何与支付系统进行同步?后台系统需要处理哪些异常情况?
  • 订单的取消、修改(如收货地址、商品数量)在后台系统中有哪些限制和处理逻辑?
  • 如何计算订单的总价(包括商品价格、运费、优惠活动等)?优惠分摊的逻辑是怎样的?
  • 订单的物流信息在后台系统中如何获取和更新?与物流服务商的接口如何对接?
  • 历史订单的存储和查询在后台系统中如何优化?涉及大量数据时如何提高查询效率?
  • 如何设计订单的反欺诈机制,识别和防范恶意订单?
  • 订单的售后服务(如退货、换货、退款)在后台系统中的处理流程是怎样的?与库存、财务等模块如何交互?

用户与账户管理

  • 电商后台系统中用户信息通常包含哪些字段?如何设计用户表的数据库结构?
  • 如何实现用户的注册、登录(包括第三方登录)功能在后台系统中的处理逻辑?
  • 怎样处理用户密码的加密存储和找回功能?
  • 电商后台如何管理用户的收货地址?地址数据的增删改查逻辑是怎样的?
  • 用户账户余额和积分的管理在后台系统中有哪些注意事项?如何保证数据的一致性?
  • 如何实现用户权限的分级管理(如普通用户、VIP 用户、管理员等)?
  • 当用户账户出现异常登录时,后台系统应如何处理和记录?
  • 电商后台如何统计用户的活跃度、留存率等指标?数据来源和计算逻辑是怎样的?
  • 用户信息修改(如手机号、邮箱)时,后台系统需要进行哪些验证和处理?
  • 如何设计用户操作日志的记录和查询功能,以满足审计和问题排查需求?

库存管理

  • 电商后台系统中库存管理的主要目标是什么?常见的库存管理策略有哪些?
  • 如何实现库存的实时更新?在高并发场景下如何保证库存数据的一致性?
  • 库存预警机制如何设计?预警的条件(如安全库存、滞销库存等)和通知方式是怎样的?
  • 多仓库库存管理在后台系统中如何实现?库存的分配和调拨逻辑是怎样的?
  • 库存盘点功能在后台系统中的实现步骤是怎样的?如何处理盘点差异?
  • 预售商品的库存管理与普通商品有何不同?后台系统需要特殊处理哪些方面?
  • 如何防止超卖现象的发生?在库存不足时,订单的处理逻辑是怎样的?
  • 库存数据与订单、采购、物流等模块的交互接口是如何设计的?
  • 对于虚拟商品(如电子卡券),库存管理的方式与实物商品有何区别?
  • 如何统计库存的周转率、缺货率等指标?数据来源和计算方法是怎样的?

支付与结算

  • 电商后台系统支持哪些支付方式?每种支付方式的对接流程和注意事项是什么?
  • 支付系统与电商后台系统的交互接口应包含哪些关键信息?如何保证支付数据的安全性?
  • 支付过程中的异步通知机制是如何实现的?后台系统如何处理重复通知和通知失败的情况?
  • 如何实现支付订单与业务订单的关联和对账功能?
  • 结算周期和结算规则在后台系统中如何配置和管理?(如供应商结算、平台佣金结算等)
  • 支付过程中的手续费计算和分摊逻辑是怎样的?如何在后台系统中实现?
  • 对于跨境支付,后台系统需要处理哪些特殊问题(如汇率转换、支付合规性等)?
  • 如何设计支付系统的异常处理和回滚机制?
  • 支付成功后,后台系统如何触发后续的业务流程(如订单发货、积分发放等)?
  • 财务对账在后台系统中的实现方式有哪些?如何保证财务数据与业务数据的一致性?

物流与供应链

  • 电商后台系统如何与物流服务商(如快递、仓储)进行接口对接?需要获取哪些物流信息?
  • 物流单号的生成和管理在后台系统中是如何实现的?如何避免重复和错误?
  • 发货流程在后台系统中的处理逻辑是怎样的?涉及哪些部门或系统的协作(如仓库、库存、订单等)?
  • 如何实现物流信息的实时追踪和更新?在后台系统中如何展示给用户和客服?
  • 退换货的物流处理在后台系统中有哪些特殊流程?如何与原订单和库存进行关联?
  • 供应链管理在电商后台系统中包括哪些主要功能?如何实现供应商管理、采购管理和库存管理的协同?
  • 如何根据商品的特性和用户地址选择合适的物流方案(如快递类型、运费模板等)?
  • 物流异常(如包裹丢失、破损)在后台系统中的处理流程是怎样的?如何与用户和物流服务商沟通协调?
  • 如何统计物流成本和物流效率(如发货时效、配送成功率等)?数据来源和分析方法是怎样的?
  • 对于海外仓和跨境物流,后台系统需要处理哪些额外的业务逻辑(如清关、关税计算等)?

营销与促销

  • 电商后台系统中常见的促销策略有哪些(如满减、打折、优惠券、秒杀、拼团等)?如何设计支持多种促销策略的模块?
  • 优惠券的生成、发放、使用和核销在后台系统中的处理流程是怎样的?
  • 促销活动的时间管理和范围管理(如针对特定用户群体、特定商品、特定时间段)如何实现?
  • 如何避免促销活动中的超卖和优惠叠加错误?后台系统的校验逻辑是怎样的?
  • 秒杀活动在后台系统中如何应对高并发场景?需要进行哪些技术优化?
  • 营销活动的效果评估指标(如转化率、客单价提升、销售额增长等)在后台系统中如何统计和分析?
  • 如何设计推荐系统与后台营销模块的集成,实现个性化的促销推荐?
  • 会员体系(如 VIP 等级、积分兑换)在后台系统中如何与营销活动结合?
  • 促销活动的库存预留和释放逻辑是怎样的?如何与库存管理模块进行交互?
  • 营销费用的预算管理和成本核算在后台系统中如何实现?

数据统计与分析

  • 电商后台系统需要统计哪些核心业务指标(如 GMV、UV、PV、转化率、复购率等)?数据采集的方式和频率是怎样的?
  • 如何设计数据报表功能,支持不同角色(如运营、管理层、客服)的个性化报表需求?
  • 数据统计中的维度和指标如何定义和管理?如何实现多维度的交叉分析?
  • 实时数据统计和离线数据统计在后台系统中的实现方式有何不同?各自的适用场景是什么?
  • 如何保证数据统计的准确性和完整性?数据清洗和校验的流程是怎样的?
  • 数据分析结果如何反馈到业务模块(如库存调整、促销策略优化等)?
  • 数据可视化在后台系统中的实现方式有哪些(如图表、仪表盘、数据大屏等)?
  • 对于海量数据的统计分析,后台系统需要进行哪些性能优化(如分布式计算、缓存、索引等)?
  • 如何设计数据权限管理,确保不同用户只能查看和操作其权限范围内的数据?
  • 数据统计分析模块与其他业务模块(如订单、商品、用户等)的数据接口是如何设计的?

其他综合问题

  • 电商后台系统在应对大促(如双 11、618)时,需要进行哪些准备工作和技术优化?
  • 如何保障电商后台系统的高可用性和容灾能力?常见的解决方案有哪些?
  • 后台系统的代码维护和版本管理有哪些最佳实践?如何保证多人协作开发的效率和代码质量?
  • 当电商业务拓展到新的领域或增加新的业务模块时,后台系统如何进行适应性改造?
  • 如何处理不同国家和地区的电商业务在后台系统中的差异化需求(如语言、货币、法规等)?
  • 电商后台系统中的日志管理有哪些重要性?如何设计日志的记录、存储和查询功能?
  • 对于第三方服务(如短信验证码、邮件通知、数据分析工具等)的接入,后台系统需要注意哪些问题?
  • 如何评估电商后台系统的性能瓶颈?常见的性能测试工具和方法有哪些?
  • 简述你在以往项目中参与过的电商后台系统开发经验,遇到过哪些挑战,是如何解决的?
  • 对于电商后台系统的未来发展趋势(如智能化、自动化、区块链应用等),你有哪些了解和思考?

参考:

前言

为什么要做设计方案

  • 设计是系统实现的蓝图
  • 设计是沟通协作的基础
  • 设计是思考的过程决定了产品的质量
    理解对齐:所有软件系统的目的都是为了实现用户需求,但实现的途径有无限种可能性(相比传统工程行业,软件的灵活性更大、知识迭代更快)。架构设计就是去选择其中一条最合适的实现途径,因此其中会涉及非常多关键的选路决策(为什么要这么拆分?为什么选择 A 技术而不是 B?)。这些重要的技术决策需要通过架构描述这种形式被记录和同步,才能让项目组所有成员对整个系统的理解对齐,形成共识。
    工作量化:项目管理最重要的步骤之一就是工时评估,它是确定项目排期和里程碑的直接依据。显然,只通过 PRD / 交互图是无法科学量化出项目工作量的,因为很难直观判断出一句简短需求或一个简单页面背后,究竟要写多少代码、实现起来难度有多大。有了清晰明确的架构之后,理论上绝大部分开发工作都能做到可见、可预测和可拆解,自然而然也就能够被更准确地量化。当然,精准的工作量评估在 IT 行业内也一直是个未解之谜,实际的工期会受太多未知因素影响,包括程序员的技能熟练度、心情好不好、有没有吃饱等。
    标准术语:编程作为一种具有创造力的工作,从某种角度看跟写科幻小说是类似的。好的科幻小说都喜欢造概念,比如三体中的智子,如果没看过小说肯定不知道这是个啥玩意儿。软件系统在造概念这一点上,相比科幻小说只有过之而无不及,毕竟小说里的世界通常还是以现实为背景,而软件中的世界就全凭造物者(程序员)的想象(建模)了。稍微复杂一点的软件系统,都会引入一些领域特定甚至全新创作的概念。为了避免在项目过程中出现鸡同鸭讲的沟通障碍和理解歧义,就必须对描述这些概念的术语进行统一。而架构的一个重要目的,就是定义和解释清楚系统中涉及的所有关键概念,并在整个架构设计和描述过程中使用标准和一致的术语,真正做到让大家的沟通都在一个频道上。
    言之有物 :就跟讨论产品交互时需要对着原型图、讨论代码细节时需要直接看代码一样,架构是在讨论一些较高维技术问题时的必要实物(具体的实物化形式就是所谓架构描述)。否则,要么一堆人对着空气谈(纸上谈兵都说不上),要么每次沟通时都重新找块白板画一画(费时费力且容易遗落信息,显然不是长久之计)。
    知识沉淀 & 新人培训:架构应该被作为与代码同等重要的文档资产持续沉淀和维护,同时也是项目新人快速理解和上手系统的重要依据。不要让你的系统跟公司内某些祖传遗留系统一样 —— 只有代码遗留了下来,架构文档却没有;只能靠一些口口相传的残留设计记忆,苦苦维系着项目的生命延续

技术方案应该包含哪些内容


  1. 背景:

    • 解决的问题:明确要解决的技术问题和产品问题的具体描述。
    • 难点和挑战:列出可能遇到的难点、挑战和限制条件。
    • 目标和关键指标:明确解决方案的目标和关键指标,例如性能要求、用户体验等。
  2. 外部依赖调研

    • 外部服务和组件:列出系统所依赖的外部服务、组件或系统,并描述其功能和接口。
    • 管理和集成策略:说明如何管理和集成外部依赖,包括版本控制、接口规范等。
  3. 业界方案调研和对比:

    • 调研结果:调研现有的业界解决方案,并总结其优缺点。
    • 对比分析:比较不同方案之间的特点、适用性和可行性。
  4. 整体设计:

    • 业务流程架构图:展示系统的业务流程和组件之间的关系。
    • 系统调用拓扑图:显示系统内部和外部的调用关系。
    • 技术架构图:描述系统的技术架构,包括各个模块、组件和数据流之间的关系。
  5. 功能设计:

    • 存储设计:定义系统中数据的存储方式和结构。
    • 接口设计:定义系统的各个模块之间的接口和通信方式。
    • 流程设计:描述系统的各个功能模块的流程和交互方式。
    • 缓存设计:确定系统中需要使用的缓存策略和机制。
  6. 非功能设计:

    • 兼容性设计:考虑系统与不同平台、浏览器或设备的兼容性。
    • 稳定性设计:定义系统的容错和恢复机制,确保系统的稳定性和可用性。
    • 扩展性设计:考虑系统的可扩展性,以便在需要时能够方便地扩展功能和容量。
    • 安全设计:定义系统的安全策略和机制,保护用户数据和系统资源。
    • 性能设计:考虑系统的性能需求,并设计相应的优化措施。
    • 部署设计:定义系统的部署架构和流程,包括服务器配置、网络拓扑等。
    • 可维护性设计:考虑系统的可维护性,包括日志记录、错误处理和调试功能。
    • 测试策略和方案:定义系统的测试策略和测试计划,包括单元测试、集成测试和系统测试等。
    • 部署和运维设计:描述系统的部署和运维策略,包括自动化部署、监控和故障处理等。
    • 风险点:识别系统设计中的潜在风险和问题,并提供相应的应对措施。
    • 监控设计和异常处理机制:
    • 监控需求:定义系统的监控需求,包括日志记录、性能监控和错误监控等。
    • 异常处理机制:描述系统对异常情况的处理方式和机制,包括错误提示、异常捕获和处理流程等。
  7. 资源清单:

    • 硬件资源:列出系统所需的硬件资源,例如服务器、存储设备等。
    • 软件资源:列出系统所需的软件资源,例如操作系统、数据库等。
    • 人力资源:确定系统开发和维护所需的人力资源,包括开发人员、测试人员等。
  8. 任务拆分和排期:

    • 任务拆分:将系统开发和实施过程分解为具体的任务和子任务。
    • 排期计划:为每个任务和子任务确定时间表和优先级。
  9. 评审记录:

    • 评审会议记录:记录技术方案评审会议的讨论和决策结果。
    • 修改和改进建议:记录评审过程中提出的修改和改进建议,并记录其处理状态。

如何评估技术方案的质量


功能性

  • 功能完整度
  • 功能正确性
  • 功能恰当性

稳定性(Dependability Criteria):

  • 可靠性(Reliability):系统处理错误和故障,保证数据完整性和可用性的能力
  • 兼容性,向前兼容性值
  • 可用性(Availability):系统在投入使用时可操作和可访问的程度。
  • 安全性(Security):系统保护用户数据和系统资源,防止未经授权的访问和恶意行为的能力

性能(Performance):

  • 响应时间(Latency):系统对请求的反应速度。
  • 吞吐量(Throughput):系统处理的工作量

成本(Cost):

  • 开发成本(Development Cost):系统的构建和开发所需的费用。
  • 部署成本(Deployment Cost):系统部署和运行所需的资源成本。
  • 升级成本(Upgrade Cost):将数据从旧系统转换到新系统,以及满足向后兼容性要求的成本。
  • 维护成本(Maintenance Cost):包括错误修复和未来功能增强的成本。
  • 运营成本(Administration Cost):运行系统的成本。

维护性(Maintainability

  • 可扩展性(Extensibility):系统添加新功能的容易程度。
  • 可修改性(Modifiability):系统更改功能的容易程度。
  • 适应性(Adaptability):系统适应不同应用领域的能力。
  • 可移植性(Portability):系统在不同计算机平台上运行的容易程度。
  • 可读性(Readability):代码的理解难度。
  • 需求可追溯性(Tracability of Requirements):代码与需求之间的映射关系
  • 可测试性

用户体验(User Experience)

  • 系统提供友好的用户界面和良好的用户交互,以提高用户满意度和使用效率

技术方案模板

** 附录:设计文档模板 **
设计文档没有定式。即使如此,笔者参考谷歌设计文档的结构和格式,并结合实际工作经验加以完善。在此提供一个可供新手参考的设计文档模版,您可以使用此文档模板作为思考的基础。通常,无须事无巨细地填写每一部分,不相关的内容直接略过即可。

计决策的合理性,同时也有助于日后迭代设计时,检查最初的假设是否仍然成立。

背景

我们要解决的问题是什么

为设计文档的目标读者提供理解详细设计所需的背景信息。按读者范围来提供背景。见上文关于目标读者的圈定。设计文档应该是“自足的”(self-contained),即应该为读者提供足够的背景知识,使其无需进一步的查阅资料即可理解后文的设计。保持简洁,通常以几段为宜,每段简要介绍即可。如果需要向读者提供进一步的信息,最好只提供链接。警惕知识的诅咒(知识的诅咒(Curse of knowledge)是一种认知偏差,指人在与他人交流的时候,下意识地假设对方拥有理解交流主题所需要的背景知识)

背景通常可以包括:
需求动机以及可能的例子。 如,“(tRPC) 微服务模式正在公司内变得流行,但是缺少一个通用的、封装了常用内部工具及服务接口的微服务框架”。 - 这是放置需求文档的链接的好地方。
此前的版本以及它们的问题。 如,“(tRPC) Taf 是之前的应用框架, 有以下特点,…………, 但是有以下局限性及历史遗留问题”。
其它已有方案, 如公司内其它方案或开源方案, “tRPC v.s. gRPC v.s. Arvo”
相关的项目,如 “tRPC 框架中可能会对接的其它 PCG 系统”
不要在背景中写你的设计,或对问题的解决思路。

难点和挑战

“解决这个问题的难点和挑战”

用几句话说明该设计文档的关键目的,让读者能够一眼得知自己是否对该设计文档感兴趣。 如:“本文描述 Spanner 的顶层设计”

目标和关键指标

继而,使用 Bullet Points 描述该设计试图达到的重要目标,如:

  • 可扩展性
  • 多版本
  • 全球分布
  • 同步复制
    非目标也可能很重要。非目标并非单纯目标的否定形式,也不是与解决问题无关的其它目标,而是一些可能是读者非预期的、本可作为目标但并没有的目标,如:
  • 高可用性
  • 高可靠性 如果可能,解释是基于哪些方面的考虑将之作为非目标。如:
  • 可维护性: 本服务只是过渡方案,预计寿命三个月,待 XX 上线运行后即可下线
    设计不是试图达到完美,而是试图达到平衡。 显式地声明哪些是目标,哪些是非目标,有助于帮助读者理解下文中设

总体设计

“我们如何解决这个问题?”

用一页描述高层设计。说明系统的主要组成部分,以及一些关键设计决策。应该说明该系统的模块和决策如何满足前文所列出的目标。

本设计文档的评审人应该能够根据该总体设计理解你的设计思路并做出评价。描述应该对一个新加入的、不在该项目工作的腾讯工程师而言是可以理解的。

推荐使用系统关系图描述设计。它可以使读者清晰地了解文中的新系统和已经熟悉的系统间的关系。它也可以包含新系统内部概要的组成模块。

注意:不要只放一个图而不做任何说明,请根据上面小节的要求用文字描述设计思想。

  • 一个示例体统关系图

  • 自举的文档结构图

  • 可能不太好的顶层设计
    不要在这里描述细节,放在下一章节中; 不要在这里描述背景,放在上一章节中。

详细设计

在这一节中,除了介绍设计方案的细节,还应该包括在产生最终方案过程中,主要的设计思想及权衡(tradeoff)。这一节的结构和内容因设计对象(系统,API,流程等)的不同可以自由决定,可以划分一些小节来更好地组织内容,尽可能以简洁明了的结构阐明整个设计。

不要过多写实现细节。就像我们不推荐添加只是说明”代码做了什么”的注释,我们也不推荐在设计文档中只说明你具体要怎么实现该系统。否则,为什么不直接实现呢? 以下内容可能是实现细节例子,不适合在设计文档中讨论:

  • ** API 的所有细节 **
  • ** 存储系统的 Data Schema **
  • ** 具体代码或伪代码 **
  • ** 该系统各模块代码的存放位置、各模块代码的布局 **
  • ** 该系统使用的编译器版本 **
    开发规范
    通常可以包含以下内容(注意,小节的命名可以更改为更清晰体现内容的标题):

** 各子模块的设计 **
阐明一些复杂模块内部的细节,可以包含一些模块图、流程图来帮助读者理解。可以借助时序图进行展现,如一次调用在各子模块中的运行过程。每个子模块需要说明自己存在的意义。如无必要,勿添模块。如果没有特殊情况(例如该设计文档是为了描述并实现一个核心算法),不要在系统设计加入代码或者伪代码。

** API 接口 **
如果设计的系统会暴露 API 接口,那么简要地描述一下 API 会帮助读者理解系统的边界。避免将整个接口复制粘贴到文档中,因为在特定编程语言中的接口通常包含一些语言细节而显得冗长,并且有一些细节也会很快变化。着重表现 API 接口跟设计最相关的主要部分即可。

** 存储 **
介绍系统依赖的存储设计。该部分内容应该回答以下问题,如果答案并非显而易见:

该系统对数据/存储有哪些要求? - 该系统会如何使用数据? - 数据是什么类型的? - 数据规模有多大? - 读写比是多少?读写频率有多高? - 对可扩展性是否有要求? - 对原子性要求是什么? - 对一致性要求是什么?是否需要支持事务? - 对可用性要求是什么? - 对性能的要求是什么? - …………
基于上面的事实,数据库应该如何选型? - 选用关系型数据库还是非关系型数据库?是否有合适的中间件可以使用? - 如何分片?是否需要分库分表?是否需要副本? - 是否需要异地容灾? - 是否需要冷热分离? - …………
数据的抽象以及数据间关系的描述至关重要。可以借助 ER 图(Entity Relationshiop) 的方式展现数据关系。

回答上述问题时,尽可能提供数据,将数据作为答案或作为辅助。 不要回答“数据规模很大,读写频繁”,而是回答“预计数据规模为 300T, 3M 日读出, 0.3M 日写入, 巅峰 QPS 为 300”。这样才能为下一步的具体数据库造型提供详细的决策依据,并让读者信服。 注意:在选型时也应包括可能会造成显著影响的非技术因素,如费用。

避免将所有数据定义(data schema)复制粘贴到文档中,因为 data schema 更偏实现细节。

其他方案
“我们为什么不用另一种方式解决问题?”

在介绍了最终方案后,可以有一节介绍一下设计过程中考虑过的其他设计方案(Alternatives Considered)、它们各自的优缺点和权衡点、以及导致选择最终方案的原因等。通常,有经验的读者(尤其是方案的审阅者)会很自然地想到一些其他设计方案,如果这里的介绍描述了没有选择这些方案的原因,就避免读者带着疑问看完整个设计再来询问作者。这一节可以体现设计的严谨性和全面性。

交叉关注点
基础设施
如果基础设施的选用需要特殊考量,则应该列出。 如果该系统的实现需要对基础设施进行增强或变更,也应该在此讨论。

可扩展性
你的系统如何扩展?横向扩展还是纵向扩展?注意数据存储量和流量都可能会需要扩展。

安全 & 隐私
项目通常需要在设计期即确定对安全性的保证,而难以事后补足。不同于其它部分是可选的,安全部分往往是必需的。即使你的系统不需要考虑安全和隐私,也需要显式地在本章说明为何是不必要的。安全性如何保证?

系统如何授权、鉴权和审计(Authorization, Authentication and Auditing, AAA)?
是否需要破窗(break-glass)机制?
有哪些已知漏洞和潜在的不安全依赖关系?
是否应该与专业安全团队讨论安全性设计评审?
……
数据完整性
如何保证数据完整性(Data Integrity)?如何发现存储数据的损坏或丢失?如何恢复?由数据库保证即可,还是需要额外的安全措施?为了数据完整性,需要对稳定性、性能、可复用性、可维护性造成哪些影响?

延迟
声明延迟的预期目标。描述预期延迟可能造成的影响,以及相关的应对措施。

冗余 & 可靠性
是否需要容灾?是否需要过载保护、有损降级、接口熔断、轻重分离?是否需要备份?备份策略是什么?如何修复?在数据丢失和恢复之间会发生什么?

稳定性
SLA 目标是什么? 如果监控?如何保证?

外部依赖

你的外部依赖的可靠性(如 SLA)如何?会对你的系统的可靠性造成何种影响?如果你的外部依赖不可用,会对你的系统造成何种影响?除了服务级的依赖外,不要忘记一些隐含的依赖,如 DNS 服务、时间协议服务、运行集群等。

任务查分和研发排期

描述时间及人力安排(如里程碑)。 这利于相关人员了解预期,调整工作计划。

遗留的问题、未来计划

未来可能的计划会方便读者更好地理解该设计以及其定位。

技术方案设计的规范与模板

技术设计基础

如何量化系统指标(SLA指标)

reliable


available


efficiency

latency and throughput

manageability


系统设计的权衡(top15 trade-off)

性能与可扩展性的权衡:提高性能可能需要牺牲一部分可扩展性,因为某些优化可能会引入复杂性或限制系统的扩展性。
可维护性与性能的权衡:某些优化措施可能会降低代码的可读性和可维护性,因此需要在维护性和性能之间进行权衡。
时间与成本的权衡:系统设计需要考虑开发时间和成本,以确保在给定资源限制下实现最佳的设计方案
安全性与用户体验的权衡:强大的安全措施可能会增加用户的身份验证和授权过程,从而影响用户体验。
架构权衡评估方法(ATAM):如何评估一个系统的质量
架构-trade-off(架构权衡
https://haomo-tech.com/project-docs/%E7%B3%BB%E7%BB%9F%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1%E6%96%87%E6%A1%A3/assets/%E7%B3%BB%E7%BB%9F%E4%B8%9A%E5%8A%A1%E6%9E%B6%E6%9E%84%E5%9B%BE.omnigraffle
架构-trade-off(架构权衡
架构权衡评估方法(ATAM):如何评估一个系统的质量
系统架构

面向对象系统设计的原则

单一职责原则(SRP):每个组件或模块应该具有单一的责任,降低耦合度,提高可维护性。
开闭原则(OCP):系统应对扩展开放,对修改关闭,通过接口和抽象来实现。
替换原则(LSP):子类应该能够替换其基类,而不会影响系统的正确性。
接口隔离原则(ISP):客户端不应该依赖于它不需要的接口,接口应该精简而专注。
依赖倒置原则(DIP):高层模块不应该依赖于低层模块,两者都应该依赖于抽象
SOLID 原则是一套比较经典且流行的架构原则(主要还是名字起得好):
单一职责:与 Unix 哲学所倡导的“Do one thing and do it well”不谋而合;
开闭原则:用新增(扩展)来取代修改(破坏现有封装),这与函数式的 immutable 思想也有异曲同工之妙;
里式替换:父类能够出现的地方子类一定能够出现,这样它们之间才算是具备继承的“Is-A”关系;
接口隔离:不要让一个类依赖另一个类中用不到的接口,简单说就是最小化组件之间的接口依赖和耦合;
依赖反转:依赖抽象类与接口,而不是具体实现;让低层次模块依赖高层次模块的稳定抽象,实现解耦
此外,我们做架构设计时也会尽量遵循如下一些原则(与上述 SOLID 原则在本质上也是相通的):
正交性:架构同一层次拆分出的各组件之间,应该尽量保持正交,即彼此职责独立,边界清晰,没有重叠;
高内聚:同一组件内部应该是高度内聚的(cohesive),像是一个不可分割的整体(否则就应该拆开);
低耦合:不同组件之间应该尽量减少耦合(coupling),既降低相互的变化影响,也能增强组件可复用性;
隔离变化:许多架构原则与模式的本质都是在隔离变化 —— 将预期可能变化的部分都隔离到一块,减少发生变化时受影响(需要修改代码、重新测试或产生故障隐患)的其他稳定部分
https://github.com/leewaiho/Clean-Architecture-zh/tree/master?tab=readme-ov-file

互联网系统八大谬论


数学估算

延迟数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Latency Comparison Numbers
--------------------------
L1 cache reference 0.5 ns
Branch mispredict 5 ns
L2 cache reference 7 ns 14x L1 cache
Mutex lock/unlock 25 ns
Main memory reference 100 ns 20x L2 cache, 200x L1 cache
Compress 1K bytes with Zippy 10,000 ns 10 us
Send 1 KB bytes over 1 Gbps network 10,000 ns 10 us
Read 4 KB randomly from SSD* 150,000 ns 150 us ~1GB/sec SSD
Read 1 MB sequentially from memory 250,000 ns 250 us
Round trip within same datacenter 500,000 ns 500 us
Read 1 MB sequentially from SSD* 1,000,000 ns 1,000 us 1 ms ~1GB/sec SSD, 4X memory
Disk seek 10,000,000 ns 10,000 us 10 ms 20x datacenter roundtrip
Read 1 MB sequentially from 1 Gbps 10,000,000 ns 10,000 us 10 ms 40x memory, 10X SSD
Read 1 MB sequentially from disk 30,000,000 ns 30,000 us 30 ms 120x memory, 30X SSD
Send packet CA->Netherlands->CA 150,000,000 ns 150,000 us 150 ms

基于上述数字的指标:

  • 从磁盘以 30 MB/s 的速度顺序读取
  • 以 100 MB/s 从 1 Gbps 的以太网顺序读取
  • 从 SSD 以 1 GB/s 的速度读取
  • 以 4 GB/s 的速度从主存读取
  • 每秒能绕地球 6-7 圈
  • 数据中心内每秒有 2,000 次往返

traffic estimates


memory estimates


bandwidth estimates


storage estimates


系统设计核心概念

📌 𝐒𝐲𝐬𝐭𝐞𝐦 𝐃𝐞𝐬𝐢𝐠𝐧 𝐊𝐞𝐲 𝐂𝐨𝐧𝐜𝐞𝐩𝐭𝐬

  • Scalability: lnkd.in/gpge_z76
  • CAP Theorem: lnkd.in/g3hmVamx
  • ACID Transactions: lnkd.in/gMe2JqaF
  • Consistent Hashing: lnkd.in/gd3eAQKA
  • Rate Limiting: lnkd.in/gWsTDR3m
  • API Design: lnkd.in/ghYzrr8q
  • Strong vs Eventual Consistency: lnkd.in/gJ-uXQXZ
  • Synchronous vs. asynchronous communications: lnkd.in/g4EqcckR
  • REST vs RPC: lnkd.in/gN__zcAB
  • Batch Processing vs Stream Processing: lnkd.in/gaAnP_fT
  • Fault Tolerance: lnkd.in/dVJ6n3wA
  • Consensus Algorithms: lnkd.in/ggc3tFbr
  • Gossip Protocol: lnkd.in/gfPMtrJZ
  • Service Discovery: lnkd.in/gjnrYkyF
  • Disaster Recovery: lnkd.in/g8rnr3V3
  • Distributed Tracing: lnkd.in/d6r5RdXG
  • Top 15 Tradeoffs: lnkd.in/gnM8QC-z

🛠️ 𝐒𝐲𝐬𝐭𝐞𝐦 𝐃𝐞𝐬𝐢𝐠𝐧 𝐁𝐮𝐢𝐥𝐝𝐢𝐧𝐠 𝐁𝐥𝐨𝐜𝐤𝐬

  • Horizontal vs Vertical Scaling: lnkd.in/gAH2e9du
  • Databases: lnkd.in/gti8gjpz
  • Content Delivery Network (CDN): lnkd.in/gjJrEJeH
  • Domain Name System (DNS): lnkd.in/gkMcZW8V
  • Caching: lnkd.in/gC9piQbJ
  • Distributed Caching: lnkd.in/g7WKydNg
  • Load Balancing: lnkd.in/gQaa8sXK
  • SQL vs NoSQL: lnkd.in/g3WC_yxn
  • Database Indexes: lnkd.in/dGnZiNmM
  • HeartBeats: lnkd.in/gfb9-hpN
  • Circuit Breaker: lnkd.in/gCxyFzKm
  • Idempotency: lnkd.in/gPm6EtKJ
  • Database Scaling: lnkd.in/gAXpSyWQ
  • Data Replication: lnkd.in/gVAJxTpS
  • Data Redundancy: lnkd.in/gNN7TF7n
  • Database Sharding: lnkd.in/gRHb-67m
  • Failover: lnkd.in/dihZ-cEG
  • Proxy Server: lnkd.in/gi8KnKS6
  • Message Queues: lnkd.in/gTzY6uk8
  • WebSockets: lnkd.in/g76Gv2KQ
  • Bloom Filters: lnkd.in/dt4QbSUz
  • API Gateway: lnkd.in/gnsJGJaM
  • Distributed Locking: lnkd.in/gRxNJwWE
  • Checksum: lnkd.in/gCTa4DrS

🖇️ 𝐒𝐲𝐬𝐭𝐞𝐦 𝐃𝐞𝐬𝐢𝐠𝐧 𝐀𝐫𝐜𝐡𝐢𝐭𝐞𝐜𝐭𝐮𝐫𝐚𝐥 𝐏𝐚𝐭𝐭𝐞𝐫𝐧𝐬

  • Client-Server Architecture: lnkd.in/dAARQYzq
  • Microservices Architecture: lnkd.in/gFXUrz_T
  • Serverless Architecture: lnkd.in/gQNAXKkb
  • Event-Driven Architecture: lnkd.in/dp8CPvey
  • Peer-to-Peer (P2P) Architecture: lnkd.in/di32HDu3

整体架构设计

软件架构模式(patterns)

Application Landscape Patterns

Application structure Patterns

User Interface Patterns

  • MVC
  • MVP

参考阅读:

架构 EA+4A

什么是架构 EA+4A

业务架构

应用架构

技术架构

数据架构

架构设计原则

扩展阅读

微服务架构

单体服务、微服务、Service Mesh


什么是服务治理

  • 单体服务(Monolithic Services):单体服务是指将整个应用程序作为一个单一的、紧密耦合的单元进行开发、部署和运行的架构模式。在单体服务中,应用程序的各个功能模块通常运行在同一个进程中,并共享相同的数据库和资源。单体服务的优点是开发简单、部署方便,但随着业务规模的增长,单体服务可能变得庞大且难以维护。

  • 微服务(Microservices):微服务是一种将应用程序拆分为一组小型、独立部署的服务的架构模式。每个微服务都专注于单个业务功能,并通过轻量级的通信机制(如RESTful API或消息队列)进行相互通信。微服务的优点是灵活性高、可扩展性好,每个微服务可以独立开发、测试、部署和扩展。然而,微服务架构也带来了分布式系统的复杂性和管理的挑战。

  • Service Mesh:Service Mesh是一种用于解决微服务架构中服务间通信和治理问题的基础设施层。它通过在服务之间插入一个专用的代理(称为Sidecar)来提供服务间的通信、安全性、可观察性和弹性的功能。Service Mesh可以提供流量管理、负载均衡、故障恢复、安全认证、监控和追踪等功能,而不需要在每个微服务中显式实现这些功能。常见的Service Mesh实现包括Istio、Linkerd和Consul Connect等。

微服务


gRPC 概述

与此讨论相关的话题是 微服务,可以被描述为一系列可以独立部署的小型的,模块化服务。每个服务运行在一个独立的线程中,通过明确定义的轻量级机制通讯,共同实现业务目标。1例如,Pinterest 可能有这些微服务: 用户资料、关注者、Feed 流、搜索、照片上传等。

服务发现

ZooKeeper

  • ZooKeeper是一个开源的分布式协调服务,最初由雅虎开发并后来成为Apache软件基金会的顶级项目。
  • ZooKeeper提供了一个分布式的、高可用的、强一致性的数据存储服务。它的设计目标是为构建分布式系统提供可靠的协调机制。
  • ZooKeeper使用基于ZAB(ZooKeeper Atomic Broadcast)协议的一致性算法来保证数据的一致性和可靠性。
  • ZooKeeper提供了一个类似于文件系统的层次化命名空间(称为ZNode),可以存储和管理数据,并支持对数据的读写操作。
  • ZooKeeper还提供了一些特性,如临时节点、顺序节点和观察者机制,用于实现分布式锁、选举算法和事件通知等。

etcd

  • etcd是一个开源的分布式键值存储系统,由CoreOS开发并后来成为Cloud Native Computing Foundation(CNCF)的项目之一。

  • etcd被设计为一个高可用、可靠的分布式存储系统,用于存储和管理关键的配置数据和元数据。

  • etcd使用Raft一致性算法来保证数据的一致性和可靠性,Raft是一种强一致性的分布式共识算法。

  • etcd提供了一个简单的键值存储接口,可以存储和检索键值对数据,并支持对数据的原子更新操作。

  • etcd还提供了一些高级特性,如目录结构、事务操作和观察者机制,用于构建复杂的分布式系统和应用

  • Etcd

  • Zookeeper

  • Consul

  • grpc

Service Mesh


service Mesh 是怎么工作的

远程过程调用协议(RPC)


Source: Crack the system design interview

在 RPC 中,客户端会去调用另一个地址空间(通常是一个远程服务器)里的方法。调用代码看起来就像是调用的是一个本地方法,客户端和服务器交互的具体过程被抽象。远程调用相对于本地调用一般较慢而且可靠性更差,因此区分两者是有帮助的。热门的 RPC 框架包括 ProtobufThriftAvro

RPC 是一个“请求-响应”协议:

  • 客户端程序 ── 调用客户端存根程序。就像调用本地方法一样,参数会被压入栈中。
  • 客户端 stub 程序 ── 将请求过程的 id 和参数打包进请求信息中。
  • 客户端通信模块 ── 将信息从客户端发送至服务端。
  • 服务端通信模块 ── 将接受的包传给服务端存根程序。
  • 服务端 stub 程序 ── 将结果解包,依据过程 id 调用服务端方法并将参数传递过去。

接入层设计:域名/代理/负载均衡

域名系统

Amazon Route 53域名系统


Amazon Route 53 工作原理

### 域名解析的过程


来源:DNS 安全介绍

域名系统是把 www.example.com 等域名转换成 IP 地址。域名系统是分层次的,一些 DNS 服务器位于顶层。当查询(域名) IP 时,路由或 ISP 提供连接 DNS 服务器的信息。较底层的 DNS 服务器缓存映射,它可能会因为 DNS 传播延时而失效。DNS 结果可以缓存在浏览器或操作系统中一段时间,时间长短取决于存活时间 TTL

  • A 记录(地址) ─ 指定域名对应的 IP 地址记录。
  • CNAME(规范) ─ 一个域名映射到另一个域名或 CNAME 记录( example.com 指向 www.example.com )或映射到一个 A 记录。
  • NS 记录(域名服务) ─ 指定解析域名或子域名的 DNS 服务器。
  • MX 记录(邮件交换) ─ 指定接收信息的邮件服务.

域名管理服务

常用命令

  • nslookup
  • dig

来源及延伸阅读

代理+负载均衡器

正向forward proxy

反向reverse proxy


负载均衡器和反向代理



来源:可扩展的系统设计模式

负载均衡器将传入的请求分发到应用服务器和数据库等计算资源。无论哪种情况,负载均衡器将从计算资源来的响应返回给恰当的客户端。负载均衡器的效用在于:

  • 防止请求进入不好的服务器
  • 防止资源过载
  • 帮助消除单一的故障点
  • SSL 终结 ─ 解密传入的请求并加密服务器响应,这样的话后端服务器就不必再执行这些潜在高消耗运算了。
  • 不需要再每台服务器上安装 X.509 证书
  • Session 留存 ─ 如果 Web 应用程序不追踪会话,发出 cookie 并将特定客户端的请求路由到同一实例。
  • 通常会设置采用工作─备用双工作 模式的多个负载均衡器,以免发生故障。

负载均衡器能基于多种方式来路由流量:

四层负载均衡

四层负载均衡根据监看传输层的信息来决定如何分发请求。通常,这会涉及来源,目标 IP 地址和请求头中的端口,但不包括数据包(报文)内容。四层负载均衡执行网络地址转换(NAT)来向上游服务器转发网络数据包。

七层负载均衡器

七层负载均衡器根据监控应用层来决定怎样分发请求。这会涉及请求头的内容,消息和 cookie。七层负载均衡器终结网络流量,读取消息,做出负载均衡判定,然后传送给特定服务器。比如,一个七层负载均衡器能直接将视频流量连接到托管视频的服务器,同时将更敏感的用户账单流量引导到安全性更强的服务器。

以损失灵活性为代价,四层负载均衡比七层负载均衡花费更少时间和计算资源,虽然这对现代商用硬件的性能影响甚微。

水平扩展

负载均衡器还能帮助水平扩展,提高性能和可用性。使用商业硬件的性价比更高,并且比在单台硬件上垂直扩展更贵的硬件具有更高的可用性。相比招聘特定企业系统人才,招聘商业硬件方面的人才更加容易。

缺陷:水平扩展

  • 水平扩展引入了复杂度并涉及服务器复制
  • 服务器应该是无状态的:它们也不该包含像 session 或资料图片等与用户关联的数据。
  • session 可以集中存储在数据库或持久化缓存(Redis、Memcached)的数据存储区中。
  • 缓存和数据库等下游服务器需要随着上游服务器进行扩展,以处理更多的并发连接。

缺陷:负载均衡器

  • 如果没有足够的资源配置或配置错误,负载均衡器会变成一个性能瓶颈。
  • 引入负载均衡器以帮助消除单点故障但导致了额外的复杂性。
  • 单个负载均衡器会导致单点故障,但配置多个负载均衡器会进一步增加复杂性。

反向代理(web 服务器)


资料来源:维基百科

反向代理是一种可以集中地调用内部服务,并提供统一接口给公共客户的 web 服务器。来自客户端的请求先被反向代理服务器转发到可响应请求的服务器,然后代理再把服务器的响应结果返回给客户端。

带来的好处包括:

  • 增加安全性 - 隐藏后端服务器的信息,屏蔽黑名单中的 IP,限制每个客户端的连接数。
  • 提高可扩展性和灵活性 - 客户端只能看到反向代理服务器的 IP,这使你可以增减服务器或者修改它们的配置。
  • 本地终结 SSL 会话 - 解密传入请求,加密服务器响应,这样后端服务器就不必完成这些潜在的高成本的操作。免除了在每个服务器上安装 X.509 证书的需要
  • 压缩 - 压缩服务器响应
  • 缓存 - 直接返回命中的缓存结果
  • 静态内容 - 直接提供静态内容
    • HTML/CSS/JS
    • 图片
    • 视频
    • 等等

负载均衡器与反向代理

  • 当你有多个服务器时,部署负载均衡器非常有用。通常,负载均衡器将流量路由给一组功能相同的服务器上。
  • 即使只有一台 web 服务器或者应用服务器时,反向代理也有用,可以参考上一节介绍的好处。
  • NGINX 和 HAProxy 等解决方案可以同时支持第七层反向代理和负载均衡。

不利之处:反向代理

  • 引入反向代理会增加系统的复杂度。
  • 单独一个反向代理服务器仍可能发生单点故障,配置多台反向代理服务器(如故障转移)会进一步增加复杂度。

来源及延伸阅读

应用层web网关设计


百亿规模API网关服务Shepherd的设计与实现

将 Web 服务层与应用层(也被称作平台层)分离,可以独立缩放和配置这两层。添加新的 API 只需要添加应用服务器,而不必添加额外的 web 服务器。用于完成基础的:

API 设计规范和管理

API 架构风格

  • RESTful API
  • GraphQL
  • RPC
  • SOA

RESTful API

  • 路径名称避免动词

    1
    2
    3
    4
    5
    路径名称避免动词
    # Good
    curl -X GET /orders
    # Bad
    curl -X GET /getOrders
  • GET 获取指定 URI 的资源信息

    1
    2
    3
    4
    5
    6
    7
    # 代表获取当前系统的所有订单信息
    curl -X GET /orders

    curl -X GET /users/{user_id}/orders

    # 代表获取指定订单编号为订单详情信息
    curl -X GET /orders/{order_id}
  • POST 通过指定的 URI 创建资源

    1
    2
    curl -X POST /orders \
    -d '{"name": "awesome", region: "A"}' \
  • PUT 创建或全量替换指定 URI 上的资源

    1
    2
    curl -X PUT http://httpbin.org/orders/1 \
    -d '{"name": "new awesome", region: "B"}' \
  • PATCH 执行一个资源的部分更新

    1
    2
    3
    4
    5
    # 代表将 id 为 1 的 order 中的 region 字段进行更改,其他数据保持不变
    curl -X PATCH /orders/{order_id} \
    -d '{name: "nameB"}' \
    curl -X order/{order_id}/name (用来重命名)
    curl -X /order/{order_id}/status(用来更改用户状态)
  • DELETE 通过指定的 URI 移除资源

    1
    2
    # 代表将id的 order 删除
    curl -X DELETE /orders/{order_id}

其它规则:
规则1:应使用连字符( - )来提高URI的可读性
规则2:不得在URI中使用下划线(_)
规则3:URI路径中全都使用小写字母

API 错误码设计规范

  1. 不论请求成功或失败,始终返回 200 http status code,在 HTTP Body 中包含用户账号没有找到的错误信息:
1
2
3
4
5
6
7
8
9
10
11
12
13
如: Facebook API 的错误 Code 设计,始终返回 200 http status code:
{
"error": {
"message": "Syntax error \"Field picture specified more than once. This is only possible before version 2.1\" at character 23: id,name,picture,picture",
"type": "OAuthException",
"code": 2500,
"fbtrace_id": "xxxxxxxxxxx"
}
}

缺点:
对于每一次请求,我们都要去解析 HTTP Body,从中解析出错误码和错误信息

  1. 返回 http 404 Not Found 错误码,并在 Body 中返回简单的错误信息:
1
2
3
4
5
如: Twitter API 的错误设计
根据错误类型,返回合适的 HTTP Code,并在 Body 中返回错误信息和自定义业务 Code

HTTP/1.1 400 Bad Request
{"errors":[{"code":215,"message":"Bad Authentication data."}]}
  1. 返回 http 404 Not Found 错误码,并在 Body 中返回详细的错误信息:
1
2
3
4
5
6
7
如: 微软 Bing API 的错误设计,会根据错误类型,返回合适的 HTTP Code,并在 Body 中返回详尽的错误信息
HTTP/1.1 400
{
"code": 100101,
"message": "Database error",
"reference": "https://github.com/xx/tree/master/docs/guide/faq/xxxx"
}
  1. 业务 Code 码设计
  • 纯数字表示
  • 不同部位代表不同的服务
  • 不同的模块(品类)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
如: 错误代码说明:100101
10: 服务
01: 某个服务下的某个模块
01: 模块下的错误码序号,每个模块可以注册 100 个错误
建议 http status code 不要太多:

200 - 表示请求成功执行
400 - 表示客户端出问题
500 - 表示服务端出问题

如果觉得这 3 个错误码不够用,可以加如下 3 个错误码:
401 - 表示认证失败
403 - 表示授权失败
404 - 表示资源找不到,这里的资源可以是 URL 或者 RESTful 资源

接口幂等性设计

幂等性的重要性

  • 提高可靠性:在网络不稳定的情况下,客户端可能会重试请求。幂等性确保重复请求不会导致意外的副作用。
  • 简化客户端代码:客户端不需要担心重复请求的副作用,从而简化了错误处理逻辑。
  • 改善用户体验:确保用户操作的可预测性,避免因重复提交表单等操作导致的错误或重复数据。

怎么实现幂等性

  • 幂等键(Idempotency Key: 由客户端生成一个唯一标识请求的ID,并在请求头中包含此ID。服务器端会检查此ID是否已处理过,如果是,则返回之前的响应。
  • 幂等令牌(Idempotency Token):在需要创建资源的请求中,通过幂等令牌保证幂等性。服务器端生成并验证令牌,确保同一令牌只能创建一个资源

中间件和存储

如何选择存储组件

内容分发网络(CDN)


来源:为什么使用 CDN

内容分发网络(CDN)是一个全球性的代理服务器分布式网络,它从靠近用户的位置提供内容。通常,HTML/CSS/JS,图片和视频等静态内容由 CDN 提供,虽然亚马逊 CloudFront 等也支持动态内容。CDN 的 DNS 解析会告知客户端连接哪台服务器。

将内容存储在 CDN 上可以从两个方面来提供性能:

  • 从靠近用户的数据中心提供资源
  • 通过 CDN 你的服务器不必真的处理请求

CDN 推送(push)

当你服务器上内容发生变动时,推送 CDN 接受新内容。直接推送给 CDN 并重写 URL 地址以指向你的内容的 CDN 地址。你可以配置内容到期时间及何时更新。内容只有在更改或新增是才推送,流量最小化,但储存最大化。

CDN 拉取(pull)

CDN 拉取是当第一个用户请求该资源时,从服务器上拉取资源。你将内容留在自己的服务器上并重写 URL 指向 CDN 地址。直到内容被缓存在 CDN 上为止,这样请求只会更慢,

存活时间(TTL)决定缓存多久时间。CDN 拉取方式最小化 CDN 上的储存空间,但如果过期文件并在实际更改之前被拉取,则会导致冗余的流量。

高流量站点使用 CDN 拉取效果不错,因为只有最近请求的内容保存在 CDN 中,流量才能更平衡地分散。

缺陷:CDN

  • CDN 成本可能因流量而异,可能在权衡之后你将不会使用 CDN。
  • 如果在 TTL 过期之前更新内容,CDN 缓存内容可能会过时。
  • CDN 需要更改静态内容的 URL 地址以指向 CDN。

来源及延伸阅读

mysql 数据库


资料来源:扩展你的用户数到第一个一千万

延伸思考和学习

  • 如何正确建表。类型选择、主键约束、not null、编码方式等
  • 外建约束、还是业务约束
  • mysql join 还是业务关联等
  • 如何使用index优化查询
  • 如何使用事物acid
  • DDL注意事项
  • 是否需呀分库分表
  • 历史数据如何处理
  • 如何扩展mysql?垂直分、水平分、主备复制、主主复制
  • 性能调优?架构优化、索引优化、sql优化、连接池优化、缓存优化

redis 键值存储系统

延伸思考和学习

  • redis 五种数据结构
  • redis 使用场景。缓存数据、计数器和限流、分布式锁、bloomfilter等
  • redis key 过期时间
  • redis 存储数据一致性的容忍度
  • redis 扩展和分不少方案
  • redis 热key和大key问题

文档类型存储(es)

延伸思考和学习

  • ES index 的mapping结构
  • setting 分片和副本机制
  • 分词器
  • 检索query dsl
  • 读写流程
  • 集群架构和规划
  • 读写优化

列型存储(hbase)


资料来源: SQL 和 NoSQL,一个简短的历史

抽象模型:嵌套的 ColumnFamily<RowKey, Columns<ColKey, Value, Timestamp>> 映射

类型存储的基本数据单元是列(名/值对)。列可以在列族(类似于 SQL 的数据表)中被分组。超级列族再分组普通列族。你可以使用行键独立访问每一列,具有相同行键值的列组成一行。每个值都包含版本的时间戳用于解决版本冲突。

Google 发布了第一个列型存储数据库 Bigtable,它影响了 Hadoop 生态系统中活跃的开源数据库 HBase 和 Facebook 的 Cassandra。像 BigTable,HBase 和 Cassandra 这样的存储系统将键以字母顺序存储,可以高效地读取键列。

列型存储具备高可用性和高可扩展性。通常被用于大数据相关存储。

来源及延伸阅读:列型存储

图数据库


资料来源:图数据库

抽象模型: 图

在图数据库中,一个节点对应一条记录,一个弧对应两个节点之间的关系。图数据库被优化用于表示外键繁多的复杂关系或多对多关系。

图数据库为存储复杂关系的数据模型,如社交网络,提供了很高的性能。它们相对较新,尚未广泛应用,查找开发工具或者资源相对较难。许多图只能通过 REST API 访问。

相关资源和延伸阅读:图

来源及延伸阅读:NoSQL

SQL 还是 NoSQL


资料来源:从 RDBMS 转换到 NoSQL

选取 SQL 的原因:

  • 结构化数据
  • 严格的模式
  • 关系型数据
  • 需要复杂的联结操作
  • 事务
  • 清晰的扩展模式
  • 既有资源更丰富:开发者、社区、代码库、工具等
  • 通过索引进行查询非常快

选取 NoSQL 的原因:

  • 半结构化数据
  • 动态或灵活的模式
  • 非关系型数据
  • 不需要复杂的联结操作
  • 存储 TB (甚至 PB)级别的数据
  • 高数据密集的工作负载
  • IOPS 高吞吐量

适合 NoSQL 的示例数据:

  • 埋点数据和日志数据
  • 排行榜或者得分数据
  • 临时数据,如购物车
  • 频繁访问的(“热”)表
  • 元数据/查找表

来源及延伸阅读:SQL 或 NoSQL

缓存redis


资料来源:可扩展的系统设计模式

缓存可以提高页面加载速度,并可以减少服务器和数据库的负载。在这个模型中,分发器先查看请求之前是否被响应过,如果有则将之前的结果直接返回,来省掉真正的处理。

数据库分片均匀分布的读取是最好的。但是热门数据会让读取分布不均匀,这样就会造成瓶颈,如果在数据库前加个缓存,就会抹平不均匀的负载和突发流量对数据库的影响。

  • 客户端缓存
    缓存可以位于客户端(操作系统或者浏览器),服务端或者不同的缓存层。
  • CDN 缓存,CDN 也被视为一种缓存。
  • Web 服务器缓存
    反向代理和缓存(比如 Varnish)可以直接提供静态和动态内容。Web 服务器同样也可以缓存请求,返回相应结果而不必连接应用服务器。
  • 应用服务缓存(本地缓存)
  • 缓存服务器(remote cache)
  • 数据库本身的缓存

延伸思考和学习

  • 本地缓存、分布式缓存
  • 缓存的TTL
  • 缓存的安全性
  • 缓存的更新模式

异步与队列


资料来源:可缩放系统构架介绍

异步工作流有助于减少那些原本顺序执行的请求时间。它们可以通过提前进行一些耗时的工作来帮助减少请求时间,比如定期汇总数据。

消息队列

消息队列接收,保留和传递消息。如果按顺序执行操作太慢的话,你可以使用有以下工作流的消息队列:

  • 应用程序将作业发布到队列,然后通知用户作业状态
  • 一个 worker 从队列中取出该作业,对其进行处理,然后显示该作业完成
    不去阻塞用户操作,作业在后台处理。在此期间,客户端可能会进行一些处理使得看上去像是任务已经完成了。例如,如果要发送一条推文,推文可能会马上出现在你的时间线上,但是可能需要一些时间才能将你的推文推送到你的所有关注者那里去。
  • kafka 是一个令人满意的简单的消息代理,但是消息有可能会丢失。
  • RabbitMQ 很受欢迎但是要求你适应「AMQP」协议并且管理你自己的节点。
  • Apache Pulsar Pulsar是一个开源的、可扩展的消息队列和流处理平台。它具有高吞吐量、低延迟和可持久化的特点,支持多租户、多数据中心和多协议等功能

任务队列 (xxl-job)


资料来源:xxl-job系统构架介绍

  • 单点调度:https://github.com/robfig/cron
  • 分布式调度:https://github.com/xuxueli/xxl-job
    将调度行为抽象形成“调度中心”公共平台,而平台自身并不承担业务逻辑,“调度中心”负责发起调度请求。将任务抽象成分散的JobHandler,交由“执行器”统一管理,“执行器”负责接收调度请求并执行对应的JobHandler中业务逻辑。因此,“调度”和“任务”两部分可以相互解耦,提高系统整体稳定性和扩展性
  • 调度模块(调度中心):
    负责管理调度信息,按照调度配置发出调度请求,自身不承担业务代码。调度系统与任务解耦,提高了系统可用性和稳定性,同时调度系统性能不再受限于任务模块;
    支持可视化、简单且动态的管理调度信息,包括任务新建,更新,删除,GLUE开发和任务报警等,所有上述操作都会实时生效,同时支持监控调度结果以及执行日志,支持执行器Failover。
  • 执行模块(执行器,executor):
    负责接收调度请求并执行任务逻辑。任务模块专注于任务的执行等操作,开发和维护更加简单和高效;
    接收“调度中心”的执行请求、终止请求和日志请求等

参考:

如果队列开始明显增长,那么队列大小可能会超过内存大小,导致高速缓存未命中,磁盘读取,甚至性能更慢。背压可以通过限制队列大小来帮助我们,从而为队列中的作业保持高吞吐率和良好的响应时间。一旦队列填满,客户端将得到服务器忙或者 HTTP 503 状态码,以便稍后重试。客户端可以在稍后时间重试该请求,也许是指数退避

延时任务调度


资料来源:lmstfy github

延时任务场景

  • 延时处理:有时候需要在某个事件发生后的一段时间内执行任务。例如,当用户提交订单后,可以设置一个延时任务,在一段时间后检查是否是支付
  • 提醒和通知:延时任务调度可用于发送提醒和通知。例如,你可以设置一个延时任务,在用户注册后的24小时内发送一封欢迎邮件,或在用户下单后的一段时间内发送订单确认通知。
  • 缓存刷新:延时任务调度可用于刷新缓存数据。当缓存过期时,可以设置一个延时任务,在一定的延时时间后重新加载缓存数据,以保持数据的新鲜性
  • 任务队列跟消息队列在使用场景上最大的区别是: 任务之间是没有顺序约束而消息要求顺序(FIFO),且可能会对任务的状态更新而消息一般只会消费不会更新。 类似 Kafka 利用消息 FIFO 和不需要更新(不需要对消息做索引)的特性来设计消息存储,将消息读写变成磁盘的顺序读写来实现比较好的性能。而任务队列需要能够任务状态进行更新则需要对每个消息进行索引,如果把两者放到一起实现则很难实现在功能和性能上兼得。比如一下场景:
  • 定时任务,如每天早上 8 点开始推送消息,定期删除过期数据等
  • 任务流,如自动创建 Redis 流程由资源创建,资源配置,DNS 修改等部分组成,使用任务队列可以简化整体的设计和重试流程
  • 重试任务,典型场景如离线图片处理

可用组件

  • redis 包括有序集合(Sorted Set)你可以使用Redis的有序集合来实现延时任务队列。将任务的执行时间作为分数(score),任务的内容作为成员(member),将任务按照执行时间排序。通过定期轮询有序集合,检查是否有任务的执行时间到达,然后执行相应的任务
  • https://github.com/bitleak/lmstfy

框架和引擎

工作流引擎与任务编排

goflow

  • 技术原理:goflow 是基于数据流编程模型的工作流引擎,采用有向无环图(DAG)描述任务节点之间的依赖关系。每个节点可独立执行,节点间通过通道(channel)传递数据,充分利用 Go 的并发特性(goroutine、channel)实现高效的任务编排和并行处理。
  • 使用场景:适用于需要复杂任务编排、数据流转、并发处理的场景,如数据处理流水线、ETL、自动化运维流程等。适合对任务依赖关系有明确建模需求的系统。

参考:goflow GitHub

go-workflow

  • 技术原理:go-workflow 是一款轻量级的工作流引擎,支持任务的定义、调度、状态管理和持久化。通过状态机管理任务流转,支持任务重试、超时、失败回调等机制。底层可集成多种存储后端(如 MySQL、Redis)以保证任务可靠性和可恢复性。
  • 使用场景:适合需要可靠任务编排、长流程管理、任务状态追踪的业务,如订单处理、审批流、异步任务调度等。适用于对任务持久化和容错有较高要求的系统。

参考:go-workflow GitHub


规则引擎与风控、资损、校验

gengine

  • 技术原理:gengine 是一款高性能、轻量级的 Go 规则引擎,采用自定义 DSL(领域特定语言)编写规则,支持动态加载和热更新。底层通过 AST(抽象语法树)解析和高效的规则匹配算法,实现复杂业务规则的快速执行。支持规则优先级、条件判断、动作执行等特性。
  • 使用场景:广泛应用于风控决策、资损防控、数据校验、营销策略等需要灵活配置和频繁变更规则的场景。适合对规则实时性和可维护性有较高要求的金融、电商等行业。

参考:gengine GitHub


脚本执行引擎与低代码平台

tengo

  • 技术原理:tengo 是一个用 Go 实现的嵌入式脚本语言,语法类似 JavaScript。支持类型安全、垃圾回收、闭包、模块化等特性。可将业务逻辑以脚本形式动态加载和执行,便于扩展和热更新。适合嵌入到 Go 应用中作为业务自定义脚本引擎。
  • 使用场景:适用于低代码平台、动态业务规则、用户自定义脚本、插件系统等场景。可用于实现灵活的业务扩展和快速迭代。

参考:tengo GitHub

anko

  • 技术原理:anko 是一个简洁的 Go 脚本解释器,支持基本的脚本语法、变量、函数、流程控制等。可与 Go 代码无缝集成,支持在运行时动态执行脚本。适合对脚本功能要求不高但需要快速集成的场景。
  • 使用场景:适合低代码平台、配置驱动、动态表达式计算、简单自动化脚本等。适用于对性能和安全性有一定要求但业务逻辑相对简单的系统。

参考:anko GitHub

好用的规范和工具

规范:

  • Go编码规范
  • api 设计规范
  • git 使用规范

工具:

云原生和服务部署CI/CD

大数据存储和计算

系统稳定性建设

影响系统可用性的因素

在系统可以用性可以做哪些工作

架构上设计 (拆分/解偶/资源隔离)

  • 支持异地多活(DR集群)
  • 服务支持横向扩容,扩容时注意事项(mysql,redis,kafka,es,依赖方,监控)
  • 离线和在线分离
  • mysql分库,分表、kafka topic、不同的ES集群
  • 辑架构和物理架构分离,订单系统支持根据业务类型路由

系统保护

  • 限流
  • 熔断降级(核心功能报错,非核心功能返回空或者固定内容)

技术选型,组件本身的可用性保证和容量评估

  • 适用性
  • 优缺点
  • 产品口碑
  • 社区活跃度
  • 实战案例
  • 扩展性等多个方面进行全量评估
  • 容量评估,mysql 一写多读,codis,kafka,ES
  • 灾备,快速恢复

功能设计时考虑

  • 接口维度的限流、用户维度限流
  • 避免单点:比如在主页设计时,主页配置数据需要写在多个redis中
  • 核心功能降级策略:redis→cdn

变更和服务扩容发布流程

  • 新版本发布兼容,数据准备,变更流程,服务发布顺序
  • 扩容时注意事项(mysql,redis,kafka,es,依赖方,监控)
  • DB变更、配置变更、组件变更

可观测性&告警

  • metric & log & trace
  • 监控体系和告警指标
  • SLA和NOC指标

可观测性、监控和告警

  • 业务层的监控,例如NOC核心指标监控,登陆、首页流量、成功率、PDP流量、成功率、下单流量、成功率、支付数量、成功率等
  • 网关的监控。所有接口的流量、成功率、耗时95线,限流监控
  • 接口详情监控:可以筛选出每个接口流量、成功率、具体错误吗、耗时均线、95线等
  • 核心功能监控:缓存命中率、变价率、数据一致性监控
  • 中间件外部组件监控:mysql、redis、kafka、es,容器资源监控等
  • 外部依赖监控:支付团队,履约团队、供应商依赖服务监控

如何搭建监控和日志系统

应该知道的安全问题

这一部分需要更多内容。一起来吧
安全是一个宽泛的话题。除非你有相当的经验、安全方面背景或者正在申请的职位要求安全知识,你不需要了解安全基础知识以外的内容:

  • 在运输和等待过程中加密
  • 对所有的用户输入和从用户那里发来的参数进行处理以防止

参考:

系统设计实践

参考:

k8s 网络

linux 虚拟网络 veth pair 和 bridge

  • Network namespace 实现网络隔离
  • Veth pair提供了一种连接两个network namespace的方法
  • Bridge 实现同一网络中多个namespace的连接
  • 添加路由信息,查看路由信息
  • iptabels 和 NAT
  • 实战练习
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
sudo ip netns add ns1
sudo ip netns add ns2
sudo ip netns add ns3

sudo brctl addbr virtual-bridge

sudo ip link add veth-ns1 type veth peer name veth-ns1-br
sudo ip link set veth-ns1 netns ns1
sudo brctl addif virtual-bridge veth-ns1-br

sudo ip link add veth-ns2 type veth peer name veth-ns2-br
sudo ip link set veth-ns2 netns ns2
sudo brctl addif virtual-bridge veth-ns2-br

sudo ip link add veth-ns3 type veth peer name veth-ns3-br
sudo ip link set veth-ns3 netns ns3
sudo brctl addif virtual-bridge veth-ns3-br


sudo ip -n ns1 addr add local 192.168.1.1/24 dev veth-ns1
sudo ip -n ns2 addr add local 192.168.1.2/24 dev veth-ns2
sudo ip -n ns3 addr add local 192.168.1.3/24 dev veth-ns3

sudo ip link set virtual-bridge up
sudo ip link set veth-ns1-br up
sudo ip link set veth-ns2-br up
sudo ip link set veth-ns3-br up
sudo ip -n ns1 link set veth-ns1 up
sudo ip -n ns2 link set veth-ns2 up
sudo ip -n ns3 link set veth-ns3 up

sudo ip netns delete ns1
sudo ip netns delete ns2
sudo ip netns delete ns3
sudo ip link set virtual-bridge down
sudo brctl delbr virtual-bridge

$ sudo ip netns exec ns1 ping 192.168.1.2
PING 192.168.1.2 (192.168.1.2): 56 data bytes
64 bytes from 192.168.1.2: seq=0 ttl=64 time=0.068 ms
--- 192.168.1.2 ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 0.060/0.064/0.068 ms
$ sudo ip netns exec ns1 ping 192.168.1.3
PING 192.168.1.3 (192.168.1.3): 56 data bytes
64 bytes from 192.168.1.3: seq=0 ttl=64 time=0.055 ms
--- 192.168.1.3 ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 0.055/0.378/1.016 ms

docker 网络 和 docker0

  • docker0网桥和缺省路由
  • docker0
  • route
  • iptables 和 nat
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 查看网桥
$ brctl show
bridge name bridge id STP enabled interfaces
docker0 8000.02421557ce52 no veth91e1730
vethc858a6a
# 查看docker 网络
docker network inspect bridge

# 查看container route信息
# 目的地址为172.17的网络不走route,其它走默认的172.17.0.1 route
$ docker exec busybox1 route -n
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
0.0.0.0 172.17.0.1 0.0.0.0 UG 0 0 0 eth0
172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 eth0

# 查看iptables
# 出口不为0docker的流量都使用SNAT
$ sudo iptables -t nat -S | grep docker
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
-A DOCKER -i docker0 -j RETURN

pod 网络

pause

1
2
3
4
5
6
7
8
$ docker ps | grep etcd
8fd1337b0bf2 73deb9a3f702 "etcd --advertise-cl…" 3 hours ago Up 3 hours k8s_etcd_etcd-minikube_kube-system_94aa022caf543792dfcddf4a2ca05a30_0
1202ef34af2b registry.k8s.io/pause:3.9 "/pause" 3 hours ago Up 3 hours k8s_POD_etcd-minikube_kube-system_94aa022caf543792dfcddf4a2ca05a30_0

$ docker inspect 8fd1337b0bf2 | grep -i networkMode
$ docker inspect 8fd1337b0bf2 | grep -i networkMode
"NetworkMode": "container:1202ef34af2b155e938cbe770870ba6c8edd3a57c88545a697816c340a6ce320",

CNI 标准和插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ ls -l /opt/cni/bin/
-rwxr-xr-x 1 root root 2660408 Nov 7 2023 bandwidth
-rwxr-xr-x 1 root root 3018552 Nov 7 2023 bridge
-rwxr-xr-x 1 root root 1984728 Nov 7 2023 cnitool
-rwxr-xr-x 1 root root 7432152 Nov 7 2023 dhcp
-rwxr-xr-x 1 root root 3096120 Nov 7 2023 firewall
-rwxr-xr-x 1 root root 2250104 Nov 7 2023 host-local
-rwxr-xr-x 1 root root 2775128 Nov 7 2023 ipvlan
-rwxr-xr-x 1 root root 2305848 Nov 7 2023 loopback
-rwxr-xr-x 1 root root 2799704 Nov 7 2023 macvlan
-rwxr-xr-x 1 root root 2615256 Nov 7 2023 portmap
-rwxr-xr-x 1 root root 2891096 Nov 7 2023 ptp
-rwxr-xr-x 1 root root 2367288 Nov 7 2023 tuning
-rwxr-xr-x 1 root root 2771032 Nov 7 2023 vlan

service 网络

背景

  • Zookeeper提供名字服务,pod自身实现负载均衡,RPC框架实现负载均衡
  • Service 为 Pods 提供的固定 IP,其他服务可以通过 Service IP 找到提供服务的Endpoints。
  • Service提供负载均衡。Service 由多个 Endpoints 组成,kubernetes 对组成 Service 的 Pods 提供的负载均衡方案,例如随机访问、robin 轮询等。
  • 暂时将Pod等同于Endpoint


实现原理

  • Service IP IP 由API server分配,写入etcd
  • Etcd 中存储service和endpoints
  • Controllermanager watch etcd的变换生成endpoints
  • node 中的kube-proxy watch service 和 endpoints的变化


kube-proxy 服务发现和负载均衡

  • Order -> item 的流程
  • 服务发现:环境变量和DNS
  • servicename.namespace.svc.cluster.local
  • kub-proxy 通过watch etcd中service和endpoint的变更,维护本地的iptables/ipvs
  • kub-proxy 通过转发规则实现service ip 到 pod ip的转发,通过规则实现负载均衡


service 类型

  • ClusterIP
  • NodePort
  • LoadBalancer

ingress 网络

背景

  • 集群外部访问集群内部资源?nodeport,loadbalancer。一个服务一个port或者一个外网IP,一个域名
  • Ingress 是 Kubernetes 中的一种 API 对象,用于管理入站网络流量,基于域名和URL路径把用户的请求转发到对应的service
  • ingress相当于七层负载均衡器,是k8s对反向代理的抽象
  • ingress负载均衡,将请求自动负载到后端的pod


实现原理

  • ingress 资源对象用于编写资源配置规则
  • Ingress-controller 监听apiserver感知集群中service和pod的变化动态更新配置规则,并重载proxy反向代理的配置
  • proxy反向代理负载均衡器,例如ngnix,接收并按照ingress定义的规则进行转发,常用的是ingress-nginx等,直接转发到pod中


支持的路由方式

  • 通过使用路径规则。例如: /app1 路径映射到一个服务,将 /app2 路径映射到另一个服务。路径匹配支持精确匹配和前缀匹配两种方式。
  • 基于主机的路由匹配。例如,可以将 app1.example.com 主机名映射到一个服务,将 app2.example.com 主机名映射到另一个服务。主机匹配也可以与路径匹配结合使用,实现更细粒度的路由控制。
  • 其他条件的路由匹配::请求方法(如 GET、POST)、请求头(如 Content-Type)、查询参数等。

docker k8s 常用命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# minikube
minikube start
minikube status
minikube ssh

# docker
docker ps # 查看所有正在运行的容器
docker ps -a # 查看所有的容器,包括正在运行的和停止的

# 用交互式的方式启动容器
docker start -ai <容器名或容器ID>

# 打开容器进行交互式终端对话框
docker exec -it <容器名或容器ID> bash

# 容器中执行命令
docker exec <容器名或容器ID> ls

参考资料

kafka 特点和使用场景

  • kafka具有高吞吐、低延迟、分布式容错、持久化、可扩展的特点,常用于系统之间的异步解偶,相比接口调用,减少单个服务的复杂性
  • 场景1: 系统间不同模块的异步解偶,例如电商系统的订单和发货
  • 场景2:系统或者用户日志的采集、异步分析、持久化
  • 场景3: 保存收集流数据,以提供之后对接的Storm或其他流式计算框架进行处理。例如风控系统
  • 异步事件系统

基本概念和组成

  • broker: Kafka 集群包含一个或多个服务器,服务器节点称为broker。broker 是消息的代理,Producers往Brokers里面的指定Topic中写消息,Consumers从Brokers里面拉取指定Topic的消息,然后进行业务处理,broker在中间起到一个代理保存消息的中转站。
  • producer和client id。生产者即数据的发布者,该角色将消息发布到Kafka的topic中。broker接收到生产者发送的消息后,broker将该消息追加到当前用于追加数据的segment文件中。生产者发送的消息,存储到一个partition中,生产者也可以指定数据存储的partition。
  • Consumer 、Consumer Group 和 group id。消费者可以从broker中读取数据。消费者可以消费多个topic中的数据。每个Consumer属于一个特定的Consumer Group。这是kafka用来实现一个topic消息的广播(发给所有的consumer)和单播(发给任意一个consumer)的手段。一个topic可以有多个CG。topic的消息会复制-给consumer。如果需要实现广播,只要每个consumer有一个独立的CG就可以了。要实现单播只要所有的consumer在同一个CG。用CG还可以将consumer进行自由的分组而不需要多次发送消息到不同的topic。
  • topic。类似于kafka中表名,每条发布到Kafka集群的消息都有一个类别,这个类别被称为Topic。(物理上不同Topic的消息分开存储,逻辑上一个Topic的消息虽然保存于一个或多个broker上但用户只需指定消息的Topic即可生产或消费数据而不必关心数据存于何处)
  • Partition 和 offset
    topic中的数据分割为一个或多个partition。每个topic至少有一个partition。每个partition中的数据使用多个segment文件存储。partition中的数据是有序的,不同partition间的数据丢失了数据的顺序。如果topic有多个partition,消费数据时就不能保证数据的顺序。在需要严格保证消息的消费顺序的场景下,需要将partition数目设为1。
  • Leader 和 follower。每个partition有多个副本,其中有且仅有一个作为Leader,Leader是当前负责数据的读写的partition。Follower跟随Leader,所有写请求都通过Leader路由,数据变更会广播给所有Follower,Follower与Leader保持数据同步。如果Leader失效,则从Follower中选举出一个新的Leader。当Follower与Leader挂掉、卡住或者同步太慢,leader会把这个follower从“in sync replicas”(ISR)列表中删除,重新创建一个Follower。
  • zookeeper。zookeeper 是一个分布式的协调组件,早期版本的kafka用zk做meta信息存储,consumer的消费状态,group的管理以及 offset的值。考虑到zk本身的一些因素以及整个架构较大概率存在单点问题,新版本中逐渐弱化了zookeeper的作用。新的consumer使用了kafka内部的group coordination协议,也减少了对zookeeper的依赖,但是broker依然依赖于ZK,zookeeper 在kafka中还用来选举controller 和 检测broker是否存活等等

可靠性语义、幂等性

生产者producer

业务上需要考关注失败、丢失、重复三个问题

  • 消费发送失败:消息写入失败是否需要ack,是否需要重试
  • 消息发送重复:同一条消息重复写入对系统产生的影响
  • 消息发送丢失:消息写入成功,但是由于kafka内部的副本、容错机制,导致消息丢失对系统产生的影响

三种语义

  • 至少一次语义(At least once semantics):如果生产者收到了Kafka broker的确认(acknowledgement,ack),并且生产者的acks配置项设置为all(或-1),这就意味着消息已经被精确一次写入Kafka topic了。然而,如果生产者接收ack超时或者收到了错误,它就会认为消息没有写入Kafka topic而尝试重新发送消息。如果broker恰好在消息已经成功写入Kafka topic后,发送ack前,出了故障,生产者的重试机制就会导致这条消息被写入Kafka两次,从而导致同样的消息会被消费者消费不止一次。每个人都喜欢一个兴高采烈的给予者,但是这种方式会导致重复的工作和错误的结果。
  • 至多一次语义(At most once semantics):如果生产者在ack超时或者返回错误的时候不重试发送消息,那么消息有可能最终并没有写入Kafka topic中,因此也就不会被消费者消费到。但是为了避免重复处理的可能性,我们接受有些消息可能被遗漏处理。
  • 精确一次语义(Exactly once semantics): 即使生产者重试发送消息,也只会让消息被发送给消费者一次。精确一次语义是最令人满意的保证,但也是最难理解的。因为它需要消息系统本身和生产消息的应用程序还有消费消息的应用程序一起合作。比如,在成功消费一条消息后,你又把消费的offset重置到之前的某个offset位置,那么你将收到从那个offset到最新的offset之间的所有消息。这解释了为什么消息系统和客户端程序必须合作来保证精确一次语义

实践
Kafka消息发送有两种方式:同步(sync)和异步(async),默认是同步方式,可通过producer.type属性进行配置。Kafka通过配置request.required.acks属性来确认消息的生产:

  • 0 —表示不进行消息接收是否成功的确认;
  • 1 —表示当Leader接收成功时确认;
  • -1—表示Leader和Follower都接收成功时确认

综上所述,有6种消息生产的情况,下面分情况来分析消息丢失的场景:

  • acks=0,不和Kafka集群进行消息接收确认,则当网络异常、缓冲区满了等情况时,消息可能丢失;
  • acks=1、同步模式下,只有Leader确认接收成功后但挂掉了,副本没有同步,数据可能丢失;

通常来说,producer 采用at least once方式

消息消费consumer

  • 重复消息的幂等性:由于生产者可能多次投递和消费者commit机制等原因,消费者重复消费是很常见的问题,需要思考系统对于幂等性的要求。在很多场景下, 比如写db、redis是天然的幂等性,某些特殊的场景,可以根据唯一id,借助例如redis判别是否消费过来实现消费者的幂等性
  • 消息丢失:评估消息丢失的影响和容忍度
  • commit:考虑auto commit 和 mannul commit

监控topic消息堆积情况(lag)

在实际业务场景中,由于consumer消费速度慢于producer的速度,会造成消息堆积,最终会导致消息过期删除丢失。业务需要监控这种lag情况,并及时告警出来。

另外需要注意的是,kafka只允许单个分区的数据被一个消费者线程消费,如果消费者越多意味着partition也要越多。

然而在分区数量有限的情况下,消费者数量也就会被限制。在这种约束下,如果消息堆积了该如何处理?

消费消息的时候直接返回,然后启动异步线程去处理消息,消息如果再处理的过程中失败的话,再重新发送到kafka中。

  • 增加分区数量
  • 优化消费速度
  • 增加并行度,找多个人消化

Rebalance 机制以及可能产生的影响

Rebalance本身是Kafka集群的一个保护设定,用于剔除掉无法消费或者过慢的消费者,然后由于我们的数据量较大,同时后续消费后的数据写入需要走网络IO,很有可能存在依赖的第三方服务存在慢的情况而导致我们超时。Rebalance对我们数据的影响主要有以下几点:

  • 数据重复消费: 消费过的数据由于提交offset任务也会失败,在partition被分配给其他消费者的时候,会造成重复消费,数据重复且增加集群压力
  • Rebalance扩散到整个ConsumerGroup的所有消费者,因为一个消费者的退出,导致整个Group进行了Rebalance,并在一个比较慢的时间内达到稳定状态,影响面较大
  • 频繁的Rebalance反而降低了消息的消费速度,大部分时间都在重复消费和Rebalance
  • 数据不能及时消费,会累积lag,在Kafka的超过一定时间后会丢弃数据
  • https://zhuanlan.zhihu.com/p/46963810

kafka是怎么做到高性能

Kafka虽然除了具有上述优点之外,还具有高性能、高吞吐、低延时的特点,其吞吐量动辄几十万、上百万。

  • 磁盘顺序写入。Kafka的message是不断追加到本地磁盘文件末尾的,而不是随机的写入。所以Kafka是不会删除数据的,它会把所有的数据都保留下来,每个消费者(Consumer)对每个Topic都有一个offset用来表示 读取到了第几条数据 。
  • 操作系统page cache,使得kafka的读写操作基本基于内存,提高读写的性能
  • 零拷贝,操作系统将数据从Page Cache 直接发送socket缓冲区,减少内核态和用户态的拷贝
  • 消息topic分区partition、segment存储,提高数据操作的并行度。
  • 批量读写和批量压缩
    Kafka速度的秘诀在于,它把所有的消息都变成一个批量的文件,并且进行合理的批量压缩,减少网络IO损耗,通过mmap提高I/O速度,写入数据的时候由于单个Partion是末尾添加所以速度最优;读取数据的时候配合sendfile直接暴力输出。
  • https://blog.csdn.net/kzadmxz/article/details/101576401

Kafka文件存储机制

  • 逻辑上以topic进行分类和分组
  • 物理上topic以partition分组,一个topic分成若干个partition,物理上每个partition为一个目录,名称规则为topic名称+partition序列号
  • 每个partition又分为多个segment(段),segment文件由两部分组成,.index文件和.log文件。通过将partition划分为多个segment,避免单个partition文件无限制扩张,方便旧的消息的清理。

kafka partition 副本ISR机制保障高可用性

  • 为了保障消息的可靠性,kafka中每个partition会设置大于1的副本数。
  • 每个patition都有唯一的leader
  • partition的所有副本称为AR。所有的副本(replicas)统称为Assigned Replicas,即AR。ISR是AR中的一个子集,由leader维护ISR列表,follower从leader同步数据有一些延迟(包括延迟时间replica.lag.time.max.ms和延迟条数replica.lag.max.messages两个维度, 当前最新的版本0.10.x中只支持replica.lag.time.max.ms这个维度),任意一个超过阈值都会把follower剔除出ISR, 存入OSR(Outof-Sync Replicas)列表,新加入的follower也会先存放在OSR中。AR=ISR+OSR
  • partition 副本同步机制。Kafka的复制机制既不是完全的同步复制,也不是单纯的异步复制。事实上,同步复制要求所有能工作的follower都复制完,这条消息才会被commit,这种复制方式极大的影响了吞吐率。而异步复制方式下,follower异步的从leader复制数据,数据只要被leader写入log就被认为已经commit,这种情况下如果follower都还没有复制完,落后于leader时,突然leader宕机,则会丢失数据。而Kafka的这种使用ISR的方式则很好的均衡了确保数据不丢失以及吞吐率
    当producer向leader发送数据时,可以通过request.required.acks参数来设置数据可靠性的级别:
    • 1(默认):这意味着producer在ISR中的leader已成功收到数据并得到确认。如果leader宕机了,则会丢失数据。
    • 0:这意味着producer无需等待来自broker的确认而继续发送下一批消息。这种情况下数据传输效率最高,但是数据可靠性确是最低的。
    • -1:producer需要等待ISR中的所有follower都确认接收到数据后才算一次发送完成,可靠性最高。但是这样也不能保证数据不丢失,比如当ISR中只有leader时(前面ISR那一节讲到,ISR中的成员由于某些情况会增加也会减少,最少就只剩一个leader),这样就变成了acks=1的情况。
  • ISR 副本选举leader
  • https://blog.csdn.net/u013256816/article/details/71091774

配置参数

  • kafka producer和consumer提供了大量打配置参数,很多问题可以通过参数来进行优化,常用了有下面参数
  • https://github.com/Shopify/sarama/blob/v1.37.2/config.go
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    c.Producer.MaxMessageBytes = 1000000
    c.Producer.RequiredAcks = WaitForLocal
    c.Producer.Timeout = 10 * time.Second
    c.Producer.Partitioner = NewHashPartitioner
    c.Producer.Retry.Max = 3
    c.Producer.Retry.Backoff = 100 * time.Millisecond
    c.Producer.Return.Errors = true
    c.Producer.CompressionLevel = CompressionLevelDefault

    c.Consumer.Fetch.Min = 1
    c.Consumer.Fetch.Default = 1024 * 1024
    c.Consumer.Retry.Backoff = 2 * time.Second
    c.Consumer.MaxWaitTime = 500 * time.Millisecond
    c.Consumer.MaxProcessingTime = 100 * time.Millisecond
    c.Consumer.Return.Errors = false
    c.Consumer.Offsets.AutoCommit.Enable = true
    c.Consumer.Offsets.AutoCommit.Interval = 1 * time.Second
    c.Consumer.Offsets.Initial = OffsetNewest
    c.Consumer.Offsets.Retry.Max = 3

kafka 常用命令

  • 创建topic

    1
    bin/kafka-topics.sh --create --topic topic-name --replication-factor 2 --partitions 3 --bootstrap-server ip:port
  • 查看topic情况

    1
    2
    bin/kafka-topics.sh --topic topic_name --describe --bootstrap-server broker 

  • 查看消费组情况

    1
    ./bin/kafka-consumer-groups.sh --describe --group group_name  --bootstrap-server brokers
  • 重置消费offsets

    1
    2
    3

    ./bin/kafka-consumer-groups.sh --group group_name --bootstrap-server brokers --reset-offsets --all-topics --to-latest --execute

推荐阅读

  1. kafka数据可靠性深度解读
  2. kafka 选举
  3. Kafka为什么吞吐量大、速度快?
  4. 简单理解 Kafka 的消息可靠性策略
  5. Bootstrap server vs zookeeper in kafka?
  6. kafka 如何保证顺序消费

基本使用

创建index,setting和mapping

1
curl -XPUT -H'Content-Type: application/json'  host/index_name?pretty=true -d@index_mapping.json 
es index example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
 {
"settings": {
"index": {
"number_of_shards": "5",
"number_of_replicas": "2"
},
"analysis": {
"filter": {
"t2sconvert": {
"convert_type": "t2s",
"type": "stconvert"
}
},
"analyzer": {
"traditional_chinese_analyzer": {
"filter": "t2sconvert",
"type": "custom",
"tokenizer": "ik_smart"
}
},
"normalizer": {
"lowercase": {
"type": "custom",
"filter": [
"lowercase"
]
}
}
}
},
"mappings": {
"_doc": {
"properties": {
"key_type": {
"type": "integer"
},
"country": {
"type": "keyword"
},
"language_code": {
"type": "keyword"
},
"level": {
"type": "integer"
},
"code": {
"type": "keyword"
},
"is_available": {
"type": "boolean"
},
"is_popular": {
"type": "boolean"
},
"pop_rank": {
"type": "integer"
},
"name": {
"properties": {
"value_in_chinese": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
},
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart"
},
"value_in_english": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"normalizer": "lowercase"
}
}
}
}
},
"display_name": {
"properties": {
"value_in_chinese": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
},
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart"
},
"value_in_english": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"normalizer": "lowercase"
}
}
}
}
},
"address": {
"properties": {
"value_in_chinese": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
},
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart"
},
"value_in_english": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"normalizer": "lowercase"
}
}
}
}
},
"city_code": {
"type": "keyword"
},
"city_name": {
"properties": {
"value_in_chinese": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
},
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart"
},
"value_in_english": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"normalizer": "lowercase"
}
}
}
},
"updated": {
"type": "date",
"format": "strict_date_optional_time||epoch_millis"
}
}
}
}
}
}

查看index,_cat 基本信息

1
curl -XGET 'host/_cat/indices/*hotel_basic_info_v2_live*(支持正则表达式)?v=true&pretty=true'

查看索引mapping信息

1
curl -XGET 'host/index_name/_mapping?pretty=true'

查看索引的setting信息

1
curl -XGET 'host/index_name/_settings?pretty=true'

通过doc id 正向查询

1
curl -XGET  'host/index/_doc/doc_id?pretty=true'

query,search,倒排查询

1
2
curl -XPOST -H'Content-Type: application/json' 'host/index_name/_search?pretty=true' -d '{
"query":{}}'

update

1
2
3
4
5
curl -XPOST  -H'Content-Type: application/json' 'host/index/_doc/doc_id/_update' -d '{
"doc": {
"price": "6500000001"
}
}'

聚合count查询

1
2
3
4
5
6
7
curl -XPOST -H'Content-Type: application/json' 'host/index_name/_count' -d '{
"query": {
"term": {
"city_name.value_in_english.keyword": "Jakarta"
}
}
}'

增加字段

1
2
3
4
5
6
7
8
curl -XPOST -H'Content-Type: application/json' 'host/index_name/_doc/_mapping' -d '{
"properties": {
"facility_codes": {
"type":"keyword"
}
}
}'

analyzer

在 Elastic Search 中,分词器起到了非常重要的作用,在定义文档结构、录入和更新文档、查询文档的时候都会用到它。例如:

1
2
3
4
5
6
7
8
9
10
武汉市长江大桥欢迎您

默认分词器:
[武, 汉, 市, 长, 江, 大, 桥, 欢, 迎, 您]

普通分词器:
[武汉, 市, 武汉市, 长江, 大桥,长江大桥, 欢迎, 您, 欢迎您]

二哈分词器:
[武汉, 市长, 江大桥, 欢迎, 您]

normalizer

alias

1
2
3
4
5
6
7
POST /_aliases
{
"actions": [
{"remove": {"index": "l1", "alias": "a1"}},
{"add": {"index": "l1", "alias": "a2"}}
]
}
1
curl -XPUT  host/index_nane/_alias/index_alias_name

query DSL

es query dsl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
{
"query": {
"function_score": {
"functions": [
{
"filter": {
"term": {
"key_type": 1
}
},
"weight": 3
},
{
"filter": {
"term": {
"key_type": 2
}
},
"weight": 2
}
],
"min_score": 0,
"query": {
"dis_max": {
"queries": [
{
"function_score": {
"functions": [
{
"filter": {
"term": {
"search_key.filed1.keyword": "querywords"
}
},
"weight": 100
},
{
"filter": {
"term": {
"search_key.filed2.keyword": "querywords"
}
},
"weight": 100
}
],
"score_mode": "max"
}
},
{
"dis_max": {
"queries": [
{
"match_phrase": {
"search_key.filed1": {
"boost": 50,
"query": "querywords"
}
}
},
{
"match_phrase": {
"search_key.field2": {
"boost": 50,
"query": "querywords"
}
}
}
]
}
},
{
"dis_max": {
"queries": [
{
"prefix": {
"search_key.filed1.keyword": {
"boost": 30,
"value": "querywords"
}
}
},
{
"prefix": {
"search_key.field2.keyword": {
"boost": 30,
"value": "querywords"
}
}
}
]
}
},
{
"dis_max": {
"queries": [
{
"match": {
"search_key.field1": {
"boost": 10,
"fuzziness": "auto:6,20",
"minimum_should_match": "3>75%",
"query": "querywords"
}
}
},
{
"match": {
"search_key.field2": {
"boost": 10,
"fuzziness": "auto:6,20",
"minimum_should_match": "3>75%",
"query": "querywords"
}
}
}
]
}
}
]
}
}
}
},
"size": 30,
"sort": [
{
"_score": {
"order": "desc"
}
},
{
"key_type": {
"order": "asc"
}
},
{
"others": {
"order": "desc"
}
}
]
}

原理

基本概念

  • 节点:分布系统都有的master节点和普通节点。类似于kafka集群都会存在的一种节点
  • master节点:用于管理索引(创建索引、删除索引)、分配分片,维护元数据
  • 协调节点:ES的特殊性,需要由一个节点汇总多个分片的query结果。节点是否担任协调节点可通过配置文件配置。例如某个节点只想做协调节点:node.master=false,node.data=false
  • ES的读写流程主要是协调节点,主分片节点、副分片节点间的相互协调。
  • ES的读取分为GET和Search两种操作。GET根据文档id从正排索引中获取内容;Search不指定id,根据关键字从倒排索引中获取内容。

写单个文档的流程

  1. 客户端向集群中的某个节点发送写请求,该节点就作为本次请求的协调节点
  2. 协调节点使用文档ID来确定文档属于某个分片,再通过集群状态中的内容路由表信息获知该分片的主分片位置,将请求转发到主分片所在节点;
  3. 主分片节点上的主分片执行写操作。如果写入成功,则它将请求并行转发到副分片所在的节点,等待副分片写入成功。所有副分片写入成功后,主分片节点向协调节点报告成功,协调节点向客户端报告成功。

读取单个文档的流程

  1. 客户端向集群中的某个节点发送读取请求,该节点就作为本次请求的协调节点;
  2. 协调节点使用文档ID来确定文档属于某个分片,再通过集群状态中的内容路由表信息获知该分片的副本信息,此时它可以把请求转发到有副分片的任意节点读取数据。
  3. 协调节点会将客户端请求轮询发送到集群的所有副本来实现负载均衡。
  4. 收到读请求的节点将文档返回给协调节点,协调节点将文档返回给客户端

Search流程

ES的Search操作分为两个阶段:query then fetch。需要两阶段完成搜索的原因是:在查询时不知道文档位于哪个分片,因此索引的所有分片都要参与搜索,然后协调节点将结果合并,在根据文档ID获取文档内容。

Query查询阶段

  1. 客户端向集群中的某个节点发送Search请求,该节点就作为本次请求的协调节点;
  2. 协调节点将查询请求转发到索引的每个主分片或者副分片中;
  3. 每个分片在本地执行查询,并使用本地的Term/Document Frequency信息进行打分,添加结果到大小为from+size的本地有序优先队列中;
  4. 每个分片返回各自优先队列中所有文档的ID和排序值给协调节点,协调节点合并这些值到自己的优先队列中,产生一个全局排序后的列表。

Fetch拉取阶段
query节点知道了要获取哪些信息,但是没有具体的数据,fetch阶段要去拉取具体的数据。相当于执行多次上面的GET流程

  1. 协调节点向相关的节点发送GET请求;
  2. 分片所在节点向协调节点返回数据;
  3. 协调阶段等待所有的文档被取得,然后返回给客户端。

es 更新和乐观锁控制

  • “_version” : 1,
  • “_seq_no” : 426,
  • “_primary_term” : 1,

性能优化

关注哪些性能指标

  • (读)query latency 1-2ms,复杂的查询可能到几十ms
  • (读)fetch latency ,QPS,读数据量,延时
  • (写)index rate,QPS,数据量,延时
  • (写)index latency
  • 存储数据量
  • 集群读写QPS,CPU、内存、存储、网络IO的监控
  • 节点维度的监控
  • index维度的监控

集群规划

  • 业务存储量,期望的SLA指标
  • 节点数量、内存、CPU数量,是否需要SSD等
  • 预留buffer,磁盘使用率达到85%、90%、95%
  • CPU使用率
  • 内存使用率
  • 冷热数据,灾备方案

settings 索引优化实践

  • 分片数量:number_of_shards,经验值:建议每个分片大小不要超过30GB。建议根据集群节点的个数规模,分片个数建议>=集群节点的个数。5节点的集群,5个分片就比较合理。注意:除非reindex操作,分片数是不可以修改的
  • 副本数量:number_of_replicas。除非你对系统的健壮性有异常高的要求,比如:银行系统。可以考虑2个副本以上。否则,1个副本足够。注意:副本数是可以通过配置随时修改的
  • refresh_interval 是一个参数,用于配置 Elasticsearch 中的索引刷新间隔。索引刷新是将内存中的数据写入磁盘以使其可搜索的过程。刷新操作会将新的文档和更新的文档写入磁盘,并使其在搜索结果中可见。默认值表示每秒执行一次刷新操作
  • 按照日期规划索引是个很好的习惯
  • 务必使用别名,ES不像mysql方面的更改索引名称。使用别名就是一个相对灵活的选择
  • setting中定义繁体全文检索时的traditional_chinese_analyzer以及一个名为lowercase的normalizer,常用于keyword类型的匹配
  • 结合profile、explain api 分析query慢的原因。search profile api
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
{
"hotel_index_20220810": {
"settings": {
"index": {
"refresh_interval": "1s",
"number_of_shards": "5",
"provided_name": "hotel_index_20220810",
"creation_date": "1660127508475",
"analysis": {
"filter": {
"t2sconvert": {
"convert_type": "t2s",
"type": "stconvert"
}
},
"normalizer": {
"lowercase": {
"filter": [
"lowercase"
],
"type": "custom"
}
},
"analyzer": {
"traditional_chinese_analyzer": {
"filter": "t2sconvert",
"type": "custom",
"tokenizer": "ik_smart"
}
}
},
"number_of_replicas": "2",
"uuid": "afdjafkdlaf",
"version": {
"created": "6080599"
}
}
}
}
}

mapping 数据模型优化

  • 不要使用默认的mapping.默认Mapping的字段类型是系统自动识别的。其中:string类型默认分成:text和keyword两种类型。如果你的业务中不需要分词、检索,仅需要精确匹配,仅设置为keyword即可。根据业务需要选择合适的类型,有利于节省空间和提升精度,如:浮点型的选择.
  • Mapping各字段的选型流程
  • 选择合理的分词器。常见的开源中文分词器包括:ik分词器、ansj分词器、hanlp分词器、结巴分词器、海量分词器、“ElasticSearch最全分词器比较及使用方法” 搜索可查看对比效果。如果选择ik,建议使用ik_max_word。因为:粗粒度的分词结果基本包含细粒度ik_smart的结果。
  • 一个字段包含多种语言:分别设置了不同的分词器。中文:ik_max_word,英语:english等
  • analyzer:表示文档写入时的分词,search_analyzer表示检索时query的分词
  • type:text,type:keyword,不分词
  • normalizer 表示英文keyword判断时不区分大小写
  • “dynamic” : “strict”
  • https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-types.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
"properties": {
"accommodation": {
"properties": {
"value_in_chinese": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
},
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart"
},
"value_in_english": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"normalizer": "lowercase"
}
},
"analyzer": "english"
},
"value_in_filipino": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
},
"analyzer": "standard"
},
"value_in_indonesian": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
},
"analyzer": "indonesian"
},
"value_in_malay": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
},
"analyzer": "standard"
},
"value_in_thai": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
},
"analyzer": "thai"
},
"value_in_tw_chinese": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
},
"analyzer": "traditional_chinese_analyzer"
},
"value_in_vietnamese": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
},
"analyzer": "standard"
}
}
}
}

数据写入优化

  1. 要不要秒级响应?Elasticsearch近实时的本质是:最快1s写入的数据可以被查询到。如果refresh_interval设置为1s,势必会产生大量的segment,检索性能会受到影响。所以,非实时的场景可以调大,设置为30s,甚至-1
  2. 能批量就不单条写入
  3. 减少副本,提升写入性能。写入前,副本数设置为0,写入后,副本数设置为原来值

读优化

  1. 分析dsl
  2. 禁用 wildcard模糊匹配,通过match_phrase和slop结合查询。
  3. 极小的概率使用match匹配
  4. 结合业务场景,大量使用filter过滤器
  5. 控制返回字段和结果,同理,ES中,_source 返回全部字段也是非必须的。要通过_source 控制字段的返回,只返回业务相关的字段。
  6. 分页深度查询和遍历.分页查询使用:from+size;遍历使用:scroll;并行遍历使用:scroll+slice

业务优化

  1. 字段抽取、倾向性分析、分类/聚类、相关性判定放在写入ES之前的ETL阶段进行
  2. 产品经理基于各种奇葩业务场景可能会提各种无理需求

SDK 使用

es migrate tools

拓展阅读

前言

  1. redis有哪些使用场景?
  2. redis五种数据结构选择以及其底层实现原理?string、hashmap、list、set、zset
  3. redis 常用命令以及时间复杂度?
  4. 如何处理可能遇到的key大面积失效的缓存雪崩,无效key缓存穿透、热key缓存击穿问题?是否需要缓存预热
  5. 如何考虑缓存和数据库一致性的问题?更新DB之后删除缓存?还是更新缓存?
  6. redis 数据持久化是怎么做的?RDB和AOF机制?
  7. redis 分布式架构,codis,rdis cluster?
  8. redis 超过使用容量时的内存淘汰策略
  9. redis 过期键的删除策略
  10. redis 的单线程架构为什么快,有哪些优势和缺点?
  11. redis & Lua ?
  12. redis 性能调优?避免大key、热key导致集群倾斜,比秒复杂命令的使用,CPU、内存、宽带的监控
  13. 实践:秒杀系统的设计和实现
  14. 实践:分布式锁,setnx,expire,del
  15. 实践:bloomfilter 和 bitmap
  16. 实线:使用redis实现微信步数排行榜

redis 使用场景

  1. 缓存数据(db,service) 的数据,提高访问效率

    • 缓存容量评估
    • 缓存过期机制,时间
    • 缓存miss,溯源和监控
    • 缓存雪崩,大面积key失效DB保护。
    • 缓存击穿:热key击穿保护
    • 缓存穿透:无效key击穿DB保护
    • 缓存更新和一致性问题
    • 缓存热key和大key问题
  2. 限流和计数。lua脚本

  3. 延时队列

    • 使用 ZSET+ 定时轮询的方式实现延时队列机制,任务集合记为 taskGroupKey
    • 生成任务以 当前时间戳 与 延时时间 相加后得到任务真正的触发时间,记为 time1,任务的 uuid 即为 taskid,当前时间戳记为 curTime
    • 使用 ZADD taskGroupKey time1 taskid 将任务写入 ZSET
    • 主逻辑不断以轮询方式 ZRANGE taskGroupKey curTime MAXTIME withscores 获取 [curTime,MAXTIME) 之间的任务,记为已经到期的延时任务(集)
    • 处理延时任务,处理完成后删除即可
    • 保存当前时间戳 curTime,作为下一次轮询时的 ZRANGE 指令的范围起点
    • https://github.com/bitleak/lmstfy
  4. 消息队列

    • redis 支持 List 数据结构,有时也会充当消息队列。使用生产者:LPUSH;消费者:RBPOP 或 RPOP 模拟队列
  5. 分布式锁:https://juejin.cn/post/6936956908007850014

  6. bloomfilter: https://juejin.cn/post/6844903862072000526

    $m = -\frac{nln(p)}{(ln2)^2}$

    $k=\frac{m}{n}ln(2)$

    1
    2
    3
    4
    n 是预期插入的元素数量(数据规模),例如 20,000,000。
    p 是预期的误判率,例如 0.001。
    m 是位数组的大小。
    k 是哈希函数的数量。

redis 5种数据类型和底层数据结构

计算所需的缓存的容量,当容量超过限制时的淘汰策略

1
2
3
4
5
6
7
8
- noeviction(默认策略):对于写请求不再提供服务,直接返回错误(DEL请求和部分特殊请求除外)
- allkeys-lru:从所有key中使用LRU算法进行淘汰
- volatile-lru:从设置了过期时间的key中使用LRU算法进行淘汰
- allkeys-random:从所有key中随机淘汰数据
- volatile-random:从设置了过期时间的key中随机淘汰
- volatile-ttl:在设置了过期时间的key中,根据key的过期时间进行淘汰,越早过期的越优先被淘汰
LFU算法是Redis4.0里面新加的一种淘汰策略。它的全称是Least Frequently Used

redis 内存淘汰策略解析

redis 过期键的删除策略

过期策略通常有以下三种:

  • 定时过期:每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即清除。该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的CPU资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。
  • 惰性过期:只有当访问一个key时,才会判断该key是否已过期,过期则清除。该策略可以最大化地节省CPU资源,却对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。
  • 定期过期:每隔一定的时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已过期的key。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。
    (expires字典会保存所有设置了过期时间的key的过期时间数据,其中,key是指向键空间中的某个键的指针,value是该键的毫秒精度的UNIX时间戳表示的过期时间。键空间是指该Redis集群中保存的所有键。)

redis 两种数据持久化的原理以及优缺点

选择local、remote、multilevel cache

双buffer vs LRU/LFU

本地缓存的双缓冲机制和本地LRU(Least Recently Used)算法都是常见的缓存优化技术,它们具有不同的优点和缺点。

  1. 双缓冲机制:

    • 优点:
      • 提高并发性能:双缓冲机制使用两个缓冲区,一个用于读取数据,另一个用于写入数据。这样可以避免读写冲突,提高了并发性能。
      • 提高数据访问效率:由于读取操作不会直接访问主缓存,而是读取缓冲区的数据,因此可以更快地获取数据。
    • 缺点:
      • 内存开销增加:双缓冲机制需要维护两个缓冲区,这会增加内存开销。
      • 数据延迟:数据更新定时同步,有一定延时。
  2. 本地LRU算法:

    • 优点:
      • 数据访问效率高:LRU算法根据数据的访问顺序进行缓存替换,将最近最少使用的数据淘汰出缓存。这样可以保留最常用的数据,提高数据的访问效率。
      • 简单有效:LRU算法的实现相对简单,只需要维护一个访问顺序链表和一个哈希表即可。
    • 缺点:
      • 缓存命中率下降:如果数据的访问模式不符合LRU算法的假设,即最近访问的数据在未来也是最有可能被访问的,那么LRU算法的效果可能不理想,缓存命中率会下降。
      • 对于热点数据不敏感:LRU算法只考虑了最近的访问情况,对于热点数据(频繁访问的数据)可能无法有效地保留在缓存中。

综合来看,双缓冲机制适用于需要提高并发性能、批量更新等场景,但会增加内存开销。本地LRU算法适用于需要提高数据访问效率的场景,但对于访问模式不符合LRU假设的情况下,缓存命中率可能下降。在实际应用中,可以根据具体需求和场景选择适合的缓存优化技术。

怎么考虑缓存和db数据一致性的问题

  • 当使用redis缓存db数据时,db数据会发生update,如何考虑redis和db数据的一致性问题呢?
  • 通常来说,对于流量较小的业务来说,可以设置较小的expire time,可以将redis和db的不一致的时间控制在一定的范围内部
  • 对于缓存和db一致性要求较高的场合,通常采用的是先更新db,再删除或者更新redis,考虑到并发性和两个操作的原子性(删除或者更新可能会失败),可以增加重试机制(双删除),如果考虑主从延时,可以引入mq做延时双删
  • http://kaito-kidd.com/2021/09/08/how-to-keep-cache-and-consistency-of-db/

缓存更新方式 优缺点
缓存模式+TTL 业务代码只更新DB,不更新cache,设置较短的TTL(通常分钟级),依靠cache过期无法找到key时回源DB,热key过期可能回导致请求大量请求击穿到DB,需要使用分布式锁或者singleflight等方式避免这种问题
定时刷新模式 定时任务异步获取DB数据刷新到cache,读请求可不回源,需要考虑刷新时间和批量读写
写DB,写cache 在并发条件下,DB写操作顺序和cache操作不同保证顺序一致性,需要增加分布式锁等操作
写DB,删除cache 删除cache可能失败,需要增加重试,重试也可能失败,比较复杂的加个MQ补偿重试

思考:

  • 对一致性要求有多强?
  • TTL 设置的时长
  • 并发冲突可能性
  • 热key缓存击穿保护

redis 怎么扩容扩容和收缩

redis 为什么使用单线程模型

缓存异常与对应的解决办法

  • 缓存雪崩问题,大面积键失效或删除
  • 缓存穿透问题,不存在key的攻击行为
  • 热点数据缓存击穿,热门key失效
  • 是否需要缓存预热
  • 缓存穿透,缓存击穿,缓存雪崩解决方案分析,https://juejin.im/post/6844903651182542856

redis 为什么这么快

  • 1、完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于 HashMap,HashMap 的优势就是查找和操作的时间复杂度都是O(1);
  • 2、数据结构简单,对数据操作也简单,Redis 中的数据结构是专门进行设计的;
  • 3、采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;
  • 4、使用多路 I/O 复用模型,非阻塞 IO;
  • 5、使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis 直接自己构建了 VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;

Redis实现分布式锁

  • Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接并不存在竞争关系Redis中可以使用SETNX命令实现分布式锁。当且仅当 key 不存在,将 key 的值设为 value。 若给定的 key 已经存在,则 SETNX 不做任何动作SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写。返回值:设置成功,返回 1 。设置失败,返回 0

redis分布式方案

  1. 单机版,并发访问有限,存储有限,单点故障。
  2. 数据持久化
  3. 主从复制。主库(写)同步到从库(读)的延时会造成数据的不一致;主从模式不具备自动容错,需要大量的人工操作
  4. 哨兵模式sentinel。在主从的基础上,实现哨兵模式就是为了监控主从的运行状况,对主从的健壮进行监控,就好像哨兵一样,只要有异常就发出警告,对异常状况进行处理。当master出现故障时,哨兵通过raft选举,leader哨兵选择优先级最高的slave作为新的master,其它slaver从新的master同步数据。哨兵解决和主从不能自动故障恢复的问题,但是同时也存在难以扩容以及单机存储、读写能力受限的问题,并且集群之前都是一台redis都是全量的数据,这样所有的redis都冗余一份,就会大大消耗内存空间
  5. codis: https://github.com/CodisLabs/codis
  6. redis cluster集群模式:集群模式时一个无中心的架构模式,将数据进行分片,分不到对应的槽中,每个节点存储不同的数据内容,通过路由能够找到对应的节点负责存储的槽,能够实现高效率的查询。并且集群模式增加了横向和纵向的扩展能力,实现节点加入和收缩,集群模式时哨兵的升级版,哨兵的优点集群都有
  7. redis 分布式架构演进
  8. Redis集群化方案对比:Codis、Twemproxy、Redis Cluster

redis & Lua

Redis 执行 Lua 脚本会以原子性方式进行,在执行脚本时不会再执行其他脚本或命令。并且,Redis 只要开始执行 Lua 脚本,就会一直执行完该脚本再进行其他操作,所以 Lua 脚本中 不能进行耗时操作 。此外,基于 Redis + Lua 的应用场景非常多,如分布式锁,限流,秒杀等等。
基于项目经验来看,使用 Redis + Lua 方案有如下注意事项:

  • 使用 Lua 脚本实现原子性操作的 CAS,避免不同客户端先读 Redis 数据,经过计算后再写数据造成的并发问题
  • 前后多次请求的结果有依赖关系时,最好使用 Lua 脚本将多个请求整合为一个;但请求前后无依赖时,使用 pipeline 方式,比 Lua 脚本方便
  • 为了保证安全性,在 Lua 脚本中不要定义自己的全局变量,以免污染 Redis 内嵌的 Lua 环境。因为 Lua 脚本中你会使用一些预制的全局变量,比如说 redis.call()
  • 注意 Lua 脚本的时间复杂度,Redis 的单线程同样会阻塞在 Lua 脚本的执行中,Lua 脚本不要进行高耗时操作
  • Redis 要求单个 Lua 脚本操作的 key 必须在同一个 Redis 节点上,因此 Redis Cluster 方式需要设置 HashTag(实际中不太建议这样操作)

redis 常用命令

  • redis-cli -h host -p port -a password
  • set key value [NX|XX] [EX seconds|PX milliseconds|EXAT unix]
  • get key
  • keys pattern,*表示通配符,表示任意字符,会遍历所有键显示所有的键列表,时间复杂度O(n),在生产环境不建议使用
  • exists key [key …]
  • 秒语法查询key的过期时间:ttl key

sdk

  • github.com/go-redis/redis

推荐阅读:

  1. https://blog.csdn.net/ThinkWon/article/details/103522351
  2. https://tech.meituan.com/2017/03/17/cache-about.html
  3. 一不小心肝出了4W字的Redis面试教程
  4. 你的 Redis 为什么变慢了?
  5. redis dbindex. https://blog.csdn.net/lsm135/article/details/52945197
  6. 颠覆认知——Redis会遇到的15个「坑」,你踩过几个?
  7. Redis最佳实践:7个维度+43条使用规范
  8. Redis为什么变慢了?
  9. redis 常用命令以及时间复杂度
  10. 单线程redis为什么快

多查看文档
MySQL 5.7 Reference Manual

数据建模

https://vertabelo.com/blog/types-data-models/
https://blog.csdn.net/zhulangfly/article/details/130432124
https://aws.amazon.com/cn/what-is/data-modeling
https://www.qlik.com/us/data-modeling

基本使用

如何建表?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
CREATE TABLE `hotel_info_tab` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`hotel_id` bigint(20) NOT NULL DEFAULT '0',
`hotel_name` varchar(64) NOT NULL DEFAULT '',
`area_code` varchar(64) NOT NULL DEFAULT '',
`phone_no` varchar(24) NOT NULL DEFAULT '',
`address` text,
`star_rating` varchar(16) NOT NULL DEFAULT '',
`popularity_score` int(11) NOT NULL DEFAULT '0',
`longitude` varchar(64) NOT NULL DEFAULT '',
`latitude` varchar(64) NOT NULL DEFAULT '',
`policies` text,
`ext_info` text,
`update_time` bigint(20) NOT NULL DEFAULT '0',
`create_time` bigint(20) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
UNIQUE KEY `uidx_hotel_id` (`hotel_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=COMPRESSED

类型选择?

  • 数值类型:int,tinyint,int(10),bigint
  • 定点数(exact-value),decimal,使用字符串存储,精度
  • 浮点数(approximate-value (floating-point)):float,double,精度缺失
  • 字符串: varchar(256),char(10)(定长,根据需要使用空格填充)
  • 文本: text,json
    1
    2
    JSON 数据类型提供了数据格式验证和以及一些内置函数帮助查询和检索。
    JSON数据类型更适合存储和处理结构化的JSON数据,而TEXT数据类型更适合存储纯文本字符串。如果你需要在数据库中存储和操作JSON数据,并且使用MySQL 5.7及更高版本,那么JSON数据类型是更好的选择。如果你只需要存储普通的文本字符串,而不需要对JSON数据进行特殊处理,那么TEXT数据类型就足够了
  • 时间time:建表时通常会带上create_time,update_time,datetime,timestamp类型,有时也会用int32和int64的时间戳类型
    1
    2
    `create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
    `update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
    通常存储的都是时间戳,需要考虑使用mysql服务器的时间还是业务的时间戳,考虑使用mysql时间戳是否会有不利的影响

primary key

  • 主键PRIMARY KEY。数据库表中对储存数据对象予以唯一和完整标识的数据列或属性的组合。一个数据列只能有一个主键,且主键的取值不能缺失,即不能为空值(Null)。主键是数据库确保数据行在整张表唯一 性的保障,即使业务上本张表没有主键,也建议添加一个自增长的ID列作为主键。设定了主键之后,在后续的删改查的时候可能更加快速以及确保操作数据范围安全。
  • 自增主键还是UUID?优缺点?怎么生成UUID?,比如item表使用自增ID,order表使用订单id,订单id可以认为是uuid。

unique key

  • 唯一性约束UNIQUE KEY:唯一性约束是很重要的特性,防止重复插入数据

关于FOREIGN KEY约束,不建议使用

int(10),bigint(20)

  • 整数类型的括号中的数字仅用于指定显示宽度,并不会影响存储范围或存储空
  • 显示宽度:括号中的数字用于指定在查询结果中显示整数类型字段时的字符个数。它可以控制字段在查询结果中的对齐和显示格式。例如,如果将一个整数字段定义为 int(3),并插入值 100,在查询时该字段将以 ‘100’ 的形式显示,左侧用空格填充以达到指定的宽度
  • 零填充:括号中的数字还可以与 ZEROFILL 属性一起使用,以实现零填充的效果。当整数类型字段定义为 int(3) ZEROFILL 时,如果插入的值不足指定的宽度,MySQL 将在左侧用零进行填充

编码方式

  • 编码方式utf8mb4:通过 show variables like ‘character_set_%’; 可以查看系统默认字符集。mysql中有utf8和utf8mb4两种编码,在mysql中请大家忘记utf8,永远使用utf8mb4。这是mysql的一个遗留问题,mysql中的utf8最多只能支持3bytes长度的字符编码,对于一些需要占据4bytes的文字,mysql的utf8就不支持了,要使用utf8mb4才行
  • COLLATE=utf8mb4_unicode_ci,所谓utf8_unicode_ci,其实是用来排序的规则。对于mysql中那些字符类型的列,如VARCHAR,CHAR,TEXT类型的列,都需要有一个COLLATE类型来告知mysql如何对该列进行排序和比较。简而言之,COLLATE会影响到ORDER BY语句的顺序,会影响到WHERE条件中大于小于号筛选出来的结果,会影响DISTINCTGROUP BYHAVING语句的查询结果。另外,mysql建索引的时候,如果索引列是字符类型,也会影响索引创建,只不过这种影响我们感知不到。总之,凡是涉及到字符类型比较或排序的地方,都会和COLLATE有关。
  • 行格式,row_format,(https://dev.mysql.com/doc/refman/5.7/en/innodb-row-format.html)
  • 10.9.1 The utf8mb4 Character Set (4-Byte UTF-8 Unicode Encoding)

关于 null 的使用

  • 除text类型外其它类型一般不使用null,都应该指定默认值
    在MySQL和许多其他数据库系统中,NULL是一个特殊的值,表示缺少值或未知值。虽然NULL在某些情况下是有用的,但由于它的特殊性,使用NULL可能会带来一些问题,因此在某些情况下不建议过度使用NULL。一般只有text类型回用到,其它都应该制定默认值
  1. 逻辑判断和比较的复杂性:由于NULL表示未知或缺少值,它的比较结果不是true也不是false,而是NULL。这意味着使用NULL进行逻辑判断和比较时需要额外的注意,可能需要使用IS NULL或IS NOT NULL等特殊的操作符。
  2. 聚合函数的结果处理:在使用聚合函数(如SUM、AVG、COUNT等)进行计算时,NULL的处理可能会产生意外的结果。通常情况下,聚合函数会忽略NULL值,因此如果某列中有NULL值,可能会导致计算结果不准确。
  3. 索引的使用限制:某些类型的索引在处理NULL值时可能会受到限制。例如,对于普通索引(B-tree索引)来说,NULL值并不会被索引,因此在查询时可能无法充分利用索引的性能优势。
  4. 查询语句的复杂性增加:当使用NULL值进行查询时,可能需要编写更复杂的查询语句来处理NULL的情况,这会增加查询的复杂性和维护成本。

虽然NULL有其合理的用途,例如表示缺失的数据或未知的值,但过度使用NULL可能会导致代码的复杂性增加、查询的不准确性和性能问题。在设计数据库模式和数据模型时,需要根据实际需求和业务逻辑合理使用NULL,并考虑到其带来的潜在问题。

存储引擎(Storage Engine) 选择

Setting the Storage Engine
MySQL支持多种存储引擎,每种存储引擎都有其特点和适用场景。以下是几种常见的MySQL存储引擎对比:

  • InnoDB:

    • 事务支持:InnoDB是MySQL默认的事务性存储引擎,支持ACID事务特性,适用于需要强一致性和事务支持的应用。
    • 行级锁定:InnoDB支持行级锁定,提供更好的并发性能。
    • 外键约束:InnoDB支持外键约束,可以保持数据完整性。
    • Crash Recovery:InnoDB具有崩溃恢复机制,能够在故障恢复时保证数据的一致性。
    • 适用场景:适用于高并发、需要事务支持和数据完整性的应用,如电子商务、在线交易等。
  • MyISAM:

    • 速度和性能:MyISAM对于读取操作有很好的性能表现,适用于读取频繁的应用。
    • 表级锁定:MyISAM使用表级锁定,对并发性能有一定影响。
    • 不支持事务:MyISAM不支持事务和崩溃恢复机制,不保证数据的完整性和一致性。
    • 全文索引:MyISAM支持全文索引,适用于对文本内容进行高效搜索的应用。
    • 适用场景:适用于读取频繁、对事务和数据完整性要求不高的应用,如博客、新闻等。
  • mysql存储引擎是插件式的,支持多种存储引擎,比较常用的是innodb和myisam

  • 存储结构上的不同:innodb数据和索引时集中存储的,myism数据和索引是分开存储的

  • 数据插入顺序不同:innodb插入记录时是按照主键大小有序插入,myism插入数据时是按照插入顺序保存的

  • 事务的支持:Innodb提供了对数据库ACID事务的支持,并且还提供了行级锁和外键的约束。MyIASM引擎不提供事务的支持,支持表级锁,不支持行级锁和外键。

  • 索引的不同:innodb主键索引是聚簇索引,非主键索引是非聚簇索引,myisam是非聚簇索引。聚簇索引的叶子节点就是数据节点,而myism索引的叶子节点仍然是索引节点,只不过是指向对应数据块的指针,InnoDB的非聚簇索引叶子节点存储的是主键,需要再寻址一次才能得到数据
    总结:

  • 是否需要支持事务?innodb

  • 并发写是不是很多?innoda

  • 读多,写少,追求读速度?myisam

索引选择

  • 唯一性约束
  • 联合索引
  • 见索引

mysql隐式类型变换(有一次面试题:存储类型和查询类型不一致会发生什么?)

在MySQL中,隐式类型转换是指在表达式或操作中自动将一个数据类型转换为另一个数据类型。MySQL会根据一组规则来执行隐式类型转换,以便执行操作或比较不同类型的数据。以下是MySQL中的一些常见的隐式类型转换规则:

  • 数值类型之间的转换:MySQL会自动将不同数值类型之间进行隐式转换,例如将整数转换为浮点数,或将较小的数值类型转换为较大的数值类型。
  • 字符串和数值类型之间的转换:MySQL会尝试将字符串转换为数值类型,或将数值类型转换为字符串。如果字符串可以解析为有效的数值,那么它将被转换为相应的数值类型。
  • 日期和时间类型之间的转换:MySQL会自动将日期和时间类型转换为其他日期和时间类型。例如,可以将日期类型转换为字符串,或将字符串转换为日期类型。
  • NULL的处理:在与其他数据类型进行操作时,MySQL会将NULL隐式转换为适当的数据类型。例如,NULL与数值类型相加时会被转换为0

mysql 线上DDL表结构变更注意事项

在MySQL中进行字段类型修改、增加字段、增加索引和删除索引时,需要注意以下事项:

  • 数据备份:在进行任何结构变更之前,务必备份数据库的数据。这样可以在出现意外情况或错误时恢复数据。
  • 考虑数据类型转换:如果要修改字段的数据类型,需要考虑可能的数据类型转换问题。确保目标数据类型能够容纳原有数据,并且进行数据类型转换时不会导致数据丢失或截断。
  • 处理依赖关系:在修改字段类型、增加字段或删除字段时,需要考虑是否存在其他对象(如视图、存储过程或触发器)依赖于该字段。如果存在依赖关系,需要先处理这些依赖关系,以免操作失败或导致不一致性。
  • 使用ALTER TABLE语句:对于字段类型修改、增加字段和删除字段操作,可以使用ALTER TABLE语句来执行。确保在执行ALTER TABLE语句之前,先检查表的当前状态和结构,以避免不必要的错误。
  • 考虑数据量和性能:在进行结构变更操作时,特别是增加字段或增加索引时,需要考虑表中的数据量和性能影响。某些操作可能需要较长时间来完成,或者会对数据库的性能产生影响。在进行这些操作时,要谨慎评估和测试,以确保不会对正常运行产生负面影响。
  • 索引的选择和删除:在增加索引时,需要根据查询需求和数据访问模式选择合适的索引类型(如B-tree索引、哈希索引等)。而在删除索引时,需要确保不会影响到相关查询的性能。在进行索引的修改和删除操作时,最好事先进行性能测试和评估。
  • 注意并发操作和锁定:某些结构变更操作可能需要锁定表或行,以确保数据的一致性。在进行这些操作时,要注意可能的并发访问冲突,并在必要时进行合理的调度和通知,以避免对系统的影响。
  • 测试和验证:在进行结构变更之后,务必进行充分的测试和验证,以确保数据库的功能和性能没有受到不良影响。验证包括执行常见的查询、操作和业务逻辑,以确保一切正常。
  • 一般要求先变更DB,再发布代码
    总之,在进行MySQL的字段类型修改、增加字段、增加索引和删除索引时,需要谨慎行事,提前做好充分的准备、备份和测试,以确保操作的成功和数据的安全性
  • 表锁定和影响:某些DDL操作可能需要锁定整个表,这可能会对其他用户的操作产生影响。请在合适的时机执行DDL操作,避免对关键业务时间或频繁访问的表造成过多的阻塞。
  • 大型表操作:对于大型表的DDL操作(如ALTER TABLE),可能会涉及大量的数据移动和重建,可能会导致长时间的操作和额外的存储空间使用。在执行这些操作之前,请确保对表的大小和操作的影响进行评估
  • 错误处理和回滚:在执行DDL操作时,要注意捕获和处理可能的错误。如果DDL操作失败,确保有适当的错误处理机制和回滚策略,以保持数据的一致性
  • 数据库备份:在执行重要的DDL操作之前,请确保对数据库进行备份,以防操作出现问题导致数据丢失或不可恢复。这可以帮助你在需要时还原到先前的状态

mysql架构扩展

关系型数据库扩展包括许多技术:主从复制主主复制联合分片非规范化SQL调优

主从复制


资料来源:可扩展性、可用性、稳定性、模式

主库同时负责读取和写入操作,并复制写入到一个或多个从库中,从库只负责读操作。树状形式的从库再将写入复制到更多的从库中去。如果主库离线,系统可以以只读模式运行,直到某个从库被提升为主库或有新的主库出现。主要的优缺点: - 读写分离提供集群的性能 - 主、从多节点,宕机容灾 - 将从库提升为主库需要额外的逻辑 - 主从延时问题,需要监控

主主复制,多主复制


资料来源:可扩展性、可用性、稳定性、模式

两个主库都负责读操作和写操作,写入操作时互相协调。如果其中一个主库挂机,系统可以继续读取和写入。

  • 多主复制
    优缺点:
  • 你需要添加负载均衡器或者在应用逻辑中做改动,来确定写入哪一个数据库。
  • 多数主-主系统要么不能保证一致性(违反 ACID),要么因为同步产生了写入延迟。
  • 随着更多写入节点的加入和延迟的提高,如何解决冲突显得越发重要
  • 多活架构

联合(垂直分实例,比如商品实例、订单实例等分开)


资料来源:扩展你的用户数到第一个一千万

优缺点:
联合(或按功能划分)将数据库按对应功能分割。例如,你可以有三个数据库:论坛用户产品,而不仅是一个单体数据库,从而减少每个数据库的读取和写入流量,减少复制延迟。较小的数据库意味着更多适合放入内存的数据,进而意味着更高的缓存命中几率。没有只能串行写入的中心化主库,你可以并行写入,提高负载能力。

  • 如果你的数据库模式需要大量的功能和数据表,联合的效率并不好。
  • 你需要更新应用程序的逻辑来确定要读取和写入哪个数据库。
  • 从两个库联结数据更复杂。
  • 联合需要更多的硬件和额外的复杂度。

分片 (水平分实例,比如订单按照用户shard)


资料来源:可扩展性、可用性、稳定性、模式

https://www.digitalocean.com/community/tutorials/understanding-database-sharding

分片将数据分配在不同的数据库上,使得每个数据库仅管理整个数据集的一个子集。以用户数据库为例,随着用户数量的增加,越来越多的分片会被添加到集群中。
类似联合的优点,分片可以减少读取和写入流量,减少复制并提高缓存命中率。也减少了索引,通常意味着查询更快,性能更好。如果一个分片出问题,其他的仍能运行,你可以使用某种形式的冗余来防止数据丢失。类似联合,没有只能串行写入的中心化主库,你可以并行写入,提高负载能力。
常见的做法是用户姓氏的首字母或者用户的地理位置来分隔用户表。

  • 你需要修改应用程序的逻辑来实现分片,这会带来复杂的 SQL 查询。
  • 分片不合理可能导致数据负载不均衡。例如,被频繁访问的用户数据会导致其所在分片的负载相对其他分片高。
  • 再平衡会引入额外的复杂度。基于一致性哈希的分片算法可以减少这种情况。
  • 联结多个分片的数据操作更复杂。
  • 分片需要更多的硬件和额外的复杂度。
  • 分片时代来临
  • 数据库分片架构
  • 一致性哈希

分表/分库/历史数据归档和路由

原文链接:https://juejin.cn/post/6844903872134135816

  • 今天,探讨一个有趣的话题:MySQL 单表数据达到多少时才需要考虑分库分表?有人说 2000 万行,也有人说 500 万行。那么,你觉得这个数值多少才合适呢?
    曾经在中国互联网技术圈广为流传着这么一个说法:MySQL 单表数据量大于 2000 万行,性能会明显下降。事实上,这个传闻据说最早起源于百度。具体情况大概是这样的,当年的 DBA 测试 MySQL性能时发现,当单表的量在 2000 万行量级的时候,SQL 操作的性能急剧下降,因此,结论由此而来。然后又据说百度的工程师流动到业界的其它公司,也带去了这个信息,所以,就在业界流传开这么一个说法。
    再后来,阿里巴巴《Java 开发手册》提出单表行数超过 500 万行或者单表容量超过 2GB,才推荐进行分库分表。对此,有阿里的黄金铁律支撑,所以,很多人设计大数据存储时,多会以此为标准,进行分表操作。那么,你觉得这个数值多少才合适呢?为什么不是 300 万行,或者是 800 万行,而是 500 万行?也许你会说这个可能就是阿里的最佳实战的数值吧?那么,问题又来了,这个数值是如何评估出来的呢?稍等片刻,请你小小思考一会儿。事实上,这个数值和实际记录的条数无关,而与 MySQL 的配置以及机器的硬件有关。因为,MySQL 为了提高性能,会将表的索引装载到内存中。InnoDB buffer size 足够的情况下,其能完成全加载进内存,查询不会有问题。但是,当单表数据库到达某个量级的上限时,导致内存无法存储其索引,使得之后的 SQL 查询会产生磁盘 IO,从而导致性能下降。当然,这个还有具体的表结构的设计有关,最终导致的问题都是内存限制。这里,增加硬件配置,可能会带来立竿见影的性能提升哈。
    那么,我对于分库分表的观点是,需要结合实际需求,不宜过度设计,在项目一开始不采用分库与分表设计,而是随着业务的增长,在无法继续优化的情况下,再考虑分库与分表提高系统的性能。对此,阿里巴巴《Java 开发手册》补充到:如果预计三年后的数据量根本达不到这个级别,请不要在创建表时就分库分表。那么,回到一开始的问题,你觉得这个数值多少才合适呢?我的建议是,根据自身的机器的情况综合评估,如果心里没有标准,那么暂时以 500 万行作为一个统一的标准,相对而言算是一个比较折中的数值。

案例1. 酒店分表:

  • 酒店数量100w, 支持8中语言,2000kw种房型,1亿的图片。支持未来3年可能扩展成:酒店数量500w, 支持8钟语言,房型1亿,图片5亿
  • 分表方式:hotel 1张表,多语言表10张表,房型表20张,图片表:100张表
  • 酒店和多语言文本垂直分表
  • 根据酒店id水平分表。
  • 如果还要继续扩展,可以重新搞一个库,酒店id从500w开始,不断扩展。增加一个数据路由的模块。

案例2. 订单分表和历史订单归档(3个月或者更长时间)

案例3. 数据历史版本记录、快照表

  • 在有些场景中,数据变更不回特别频繁,特别是人工变更时,记录数据版本和快照是非常好的习惯,方便追溯历史行为记录
  • 数据变更时通常会先写入快照表或者历史记录表,通常在业务代码中实现
  • 有时也会采用mysql 存储过程实现:https://blog.csdn.net/wcdunf/article/details/129792810

案例4. 商品库存扣减方案

添加索引index,优化访问速度

  • 关于MySQL索引那些事
  • 什么是索引,对索引的理解,索引时一种数据结构,通过增加索引通常可以提高数据库查询的效率,但是为了维护索引结构也会降低数据更新的效率和增加一些存储代价。
  • 索引类型
    1
    2
    3
    4
    5
    普通索引(INDEX):最基本的索引,没有任何限制
    唯一索引(UNIQUE):与"普通索引"类似,不同的就是:索引列的值必须唯一,但允许有空值。
    主键索引(PRIMARY):它 是一种特殊的唯一索引,不允许有空值。
    全文索引(FULLTEXT ):仅可用于 MyISAM 表, 用于在一篇文章中,检索文本信息的, 针对较大的数据,生成全文索引很耗时好空间。
    组合索引:为了更多的提高mysql效率可建立组合索引,遵循”最左前缀“原则。
  • 理解主键索引和普通索引、聚簇索引和非聚簇索引、单列索引和联合索引、覆盖索引和回表
    1
    2
    3
    4
    5
    - 主键索引和普通索引。数据和主键索引用B+Tree来组织的,没有主键innodb会生成唯一列,类似于rowid。InnoDB非主键索引的叶子节点存储的是主键
    - 单列索引和联合索引,联合索引的存储结构,联合索引的左前缀原则
    - 聚簇索引和非聚簇索引,聚簇索引数据和索引一起存储,非聚簇索引在无法做到索引覆盖的情况下需要回表
    - 覆盖索引。覆盖索引(covering index)指一个查询语句的执行只用从索引中就能够取得,不必从数据表中读取。也可以称之为实现了索引覆盖。
    如果一个索引包含了(或覆盖了)满足查询语句中字段与条件的数据就叫做覆盖索引
  • 索引的数据结构,红黑树、B树、B+树的比较
  • 面试题:InnoDB中一棵B+树能存多少行数据?计算innob的高度
  • 列出索引失效的几种场景?
    • 条件中包含or
    • 条件中包含%like
    • 联合索引,违背最左匹配原则
    • 在索引列上有一些额外的计算操作
  • 联合索引和最左匹配原则

ACID、事务、数据库锁(数据准确性和并发安全,一锁二判三更新)

  • 精读mysql事务
  • innodb事务的ACID特性,以及其对应的实现原理?
    • 原子性:在很多场景中,一个操作需要执行多条 update/insert SQL。原子性保证了SQL语句要么全执行,要么全不执行,是事务最核心的特性,事务本身就是以原子性来定义的;实现主要基于undolog/redolog
    • 持久性:保证事务提交后不会因为宕机等原因导致数据丢失;实现主要基于redo log
    • 隔离性:保证事务执行尽可能不受其他事务影响;InnoDB默认的隔离级别是RR,RR的实现主要基于锁机制(包含next-key lock)、MVCC(包括数据的隐藏列、基于undo log的版本链、ReadView)
    • 一致性:事务追求的最终目标,一致性的实现既需要数据库层面的保障,也需要应用层面的保障
  • innodb四种隔离属性以及分别会产生什么问题?分别举例说明
    • 读未提交(READ UNCOMMITTED),会产生脏读问题
    • 读提交,READ-COMMITTED,会产生不可重复读问题
    • 可重复读 (REPEATABLE READ),幻读问题(insert),mysql 默认的事务隔离级别
    • SERIALIZABLE(可串行化)
  • 事务的隔离属性底层实现原理,关于锁和mvcc
    • 可以先阐述四种隔离级别,再阐述它们的实现原理。隔离级别就是依赖锁和MVCC实现的。
  • 悲观锁与乐观锁
    • Select for update使用详解 及在库存和金钱系统上的应用
    • 悲观锁:悲观锁是一种保守的并发控制机制,它假设在并发访问中会发生冲突,因此在访问数据之前会锁定资源,阻止其他事务对资源进行修改。在MySQL中,悲观锁主要通过以下方式实现:
      • 使用SELECT … FOR UPDATE语句:在读取数据时对所选行进行锁定,确保其他事务不能对这些行进行修改。
      • 使用LOCK TABLES语句:锁定整个表,防止其他事务对该表进行读取和修改。
    • 乐观锁:乐观锁是一种乐观的并发控制机制,它假设在并发访问中不会发生冲突,允许多个事务同时访问资源。当提交事务时,系统会检查资源是否被其他事务修改,如果检测到冲突,则回滚事务。在MySQL中,乐观锁通常通过以下方式实现:
      • 使用版本号或时间戳:在数据表中增加一个版本号或时间戳字段,每次修改数据时更新该字段。在提交事务时,检查版本号或时间戳是否与开始事务时的值相同,如果不同则表示发生了冲突。
      • 使用CAS(Compare and Swap)操作:在编程语言层面,通过CAS操作来比较内存中的值与预期值是否相等,如果相等则修改,否则放弃修改。
        使用乐观锁和悲观锁的选择取决于应用场景和需求:悲观锁适合在并发冲突频繁的情况下,通过独占资源避免并发问题,但会对系统性能产生一定的影响。乐观锁适合在并发冲突较少的情况下,通过乐观的并发控制机制提高系统性能,但需要处理冲突的情况。在实际使用时,需要根据具体业务场景和需求选择适当的并发控制机制,并注意处理冲突和回滚事务的策略,以确保数据的一致性和完整性。
  • 死锁问题,如何避免死锁
    • 死锁的条件:
      • 事务并发执行:多个事务同时操作相同的数据,请求相同或不同的锁资源。
      • 锁竞争:事务之间竞争相同的资源而产生死锁。
      • 不同的锁顺序:不同的事务以不同的顺序请求锁资源,导致死锁。
    • 避免死锁的方法:
      • 统一锁资源访问顺序:对于需要操作多个锁资源的事务,保持统一的访问顺序,避免不同事务之间出现交叉的锁请求顺序
      • 减少事务持有时间:尽量将事务的持有时间缩短,减少锁资源的占用时间,降低死锁的概率。
      • 使用合理的索引:合理的索引设计可以减少查询中的锁竞争,提高并发性能,减少死锁的可能性。
      • 限制事务并发度:通过调整事务的并发度,限制同时执行的事务数量,减少锁竞争的机会。
  • 分布式事务

数据库调优

  • 优化的步骤

    • 考虑数据量大导致的性能问题,访问量大导致的性能问题?
    • sql语句优化。分析执行计划,减少load的数据量
    • 考虑能否通过增加索引优化查询效率,检查索引是否生效
    • 是否有缓存
    • 垂直分表、水平分表、分库
    • 根据场景来看,写操作多的情况下,考虑读写分离
    • 数据归档:数据是否有冷热的区别,例如订单数据有比较明显的时间冷热的区别,可以考虑冷数据归档。比如半年前的订单数据可以写入hbase
    • 池化
  • 架构优化

    • 分库,分表。垂直分,水平分。依据QPS和耗时,服务端最大并非连接数量
    • 读写分离
    • 批量读写,批量更新
    • 异步写,写平滑
    • 缓存优化
    • 历史数据归档
  • 连接池的配置和使用

    • 连接池能减少连接创建和释放带来的开销,大多数SDK也支持是支持连接池的,通常实际生产环境中也都会使用到连接池,需要关注一下几个参数
    • max_idle_connections: 最大空闲连接数
    • max_open_connections: 最大连接数
    • connection_max_lifetime: 连接最大可重用时间
    • 要使用好连接池,除了关注客户端的配置还需要关注mysql服务端的配置
    • 服务端最大连接数量:show variables like ‘%connection%’; max_connections
    • 服务端连接最大生命周期:show variables like ‘%wait_timeout%’
      1
      2
      3
      最大空闲连接数 =(QPS*请求平均耗时)/ 应用节点个数
      最大连接数 =(QPS*请求最大耗时)/ 应用节点个数
      客户端连接maxlifetime < 数据库服务端设置的connection_max_lifttime
  • 慢sql优化

    • 慢查询问题,查看慢查询设置的阈值。show variables like ‘%long_query%’;
    • 打开慢查询日志
    • 分析数据sql的结构是否加载了不必要的字段和数据
    • 深度分页查询优化
    • 子查询和连接查询
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
            	explain select * from test_xxxx_tab txt order by id limit 10000,10;
      explain SELECT * from test_xxxx_tab txt where id >= (select id from test_xxxx_tab txt order by id limit 10,1) limit 10;
      id列:在复杂的查询语句中包含多个查询使用id标示
      select_type:select/subquery/derived/union
      table: 显示对应行正在访问哪个表
      type:访问类型,关联类型。非常重要,All,index,range,ref,const,
      possible_keys: 显示可以使用哪些索引列
      key列:显示mysql决定使用哪个索引来优化对该表的访问
      key_len:显示在索引里使用的字节数
      rows:为了找到所需要的行而需要读取的行数
    • 慢查询日志样例子
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      # Time: 2022-05-10T10:15:32.123456Z
      # User@Host: myuser[192.168.0.1] @ localhost [] Id: 12345
      # Query_time: 3.456789 Lock_time: 0.123456 Rows_sent: 10 Rows_examined: 100000
      SET timestamp=1657475732;
      SELECT * FROM orders WHERE customer_id = 1001 ORDER BY order_date DESC LIMIT 10;
      这个慢查询日志示例包含以下重要的信息:

      时间戳(Time): 日志记录的时间,以 UTC 时间表示。
      用户和主机(User@Host): 执行查询的用户和主机地址。
      连接 ID(Id): 表示执行查询的连接 ID。
      查询时间(Query_time): 查询执行所花费的时间,以秒为单位。
      锁定时间(Lock_time): 在执行查询期间等待锁定资源所花费的时间,以秒为单位。
      返回行数(Rows_sent): 查询返回的结果集中的行数。
      扫描行数(Rows_examined): 在执行查询过程中扫描的行数。
      时间戳(SET timestamp): 查询开始执行的时间戳。
      查询语句(SELECT * FROM orders WHERE customer_id = 1001 ORDER BY order_date DESC LIMIT 10): 实际执行的查询语句
  • index优化

    • 会查看sql执行计划explain
    • 关注:type、const、ref
    • 关注:extra等字段
  • 使用缓存优化DB需要考虑的问题

    • 缓存更新、过期、淘汰的策略
    • 缓存可能遇到的三大问题,雪崩、穿透、击穿
    • 缓存和db的一致性问题,缓存更新策略及其分析?,业界比较通用的先更新DB,再删除cache
  • 库表优化/分表/分库

    • 垂直分表
    • 水平分表
    • 分库
    • 业界成熟的方案
  • 架构优化读写分离优化

    • 在写操作的较多的情况可以考虑数据库读写分离的方案
    • 业界的方案,代理实现和业务实现
  • 核心监控告警指标

    • read write qps 监控/select/update/insert
    • connections
    • thread
    • InnoDB buffer pool
    • 慢查询监控
    • 网络流量IO
    • 读写分离架构时需要监控主从延时
  • 关键配置查看

    1
    2
    3
    4
    5
    show global variables;
    show variables like '%max_connection%'; 查看最大连接数
    show status like 'Threads%';
    show processlist;
    show variables like '%connection%';
  • 存储空间information_schema

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    -- desc information_schema.tables;
    -- 查看 MySQL「所有库」的容量大小
    SELECT table_schema AS '数据库', SUM(table_rows) AS '记录数',
    SUM(truncate(data_length / 1024 / 1024, 2)) AS '数据容量(MB)',
    SUM(truncate(index_length / 1024 / 1024, 2)) AS '索引容量(MB)',
    SUM(truncate(DATA_FREE / 1024 / 1024, 2)) AS '碎片占用(MB)'
    FROM information_schema.tables
    GROUP BY table_schema
    ORDER BY SUM(data_length) DESC, SUM(index_length) DESC;
    -- 指定书库查看表的数据量
    SELECT
    table_schema as '数据库',
    table_name as '表名',
    table_rows as '记录数',
    truncate(data_length/1024/1024, 2) as '数据容量(MB)',
    truncate(index_length/1024/1024, 2) as '索引容量(MB)',
    truncate(DATA_FREE/1024/1024, 2) as '碎片占用(MB)'
    from
    information_schema.tables
    where
    table_schema='<数据库名>'
    order by
    data_length desc, index_length desc;

MySQL多表关联查询 vs 多次单表查询service组装

  • 多次单表查询+Service组装:
    • 灵活性:多次单表查询+Service组装方式更加灵活,可以根据具体需求灵活组装和调整查询逻辑,适应各种复杂的查询需求。
    • 可扩展性:通过多次单表查询和Service组装,可以将查询逻辑分解为多个简单的查询,有助于代码的模块化和可扩展性,方便后续的维护和修改。
    • 缓存利用:多次单表查询+Service组装方式可以更好地利用缓存,针对每个单表查询的结果进行缓存,提高查询性能
      https://www.zhihu.com/question/68258877

mysql binlog

show processlist;

常用命令

  • mysql登陆:
    mysql -h主机 -P端口 -u用户 -p密码
    SET PASSWORD FOR ‘root‘@’localhost’ = PASSWORD(‘root’);
    create database wxquare_test;
    show databases;
    use wxquare_test;
  • 查看见表sql:show create table table_name;
  • show variables like ‘%timeout%’;
  • update json 文本需要转义
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
      update table set extinfo='{
    \"urls\": [
    {
    \"url\": \"/path1\",
    \"type\": \"type1\"
    },
    {
    \"url\": \"/path2\",
    \"type\": \"type2\"
    },
    ]
    }' where id = 2;
  • truncate table 属于ddl语句,需要ddl的权限
  • mysqldump 库表结构
    1
    mysqldump --column-statistics=0 -hhost -PPort -uuser_name -ppassword --databases -d db_name --skip-lock-tables --skip-add-drop-table --set-gtid-purged=OFF | sed 's/ AUTO_INCREMENT=	[0-9]*//g' > db.sql

    - 批量更新
    1
    2
    3
    4
    5
    6
    7
    8
    UPDATE employees
    SET salary = CASE
    WHEN grade = 'A' THEN salary * 1.1
    WHEN grade = 'B' THEN salary * 1.05
    WHEN grade = 'C' THEN salary * 1.03
    ELSE salary
    END
    WHERE department = 'IT';

推荐阅读:

Go 和 C++ 语言对比

Go and C++ are two different programming languages with different design goals, syntax, and feature sets. Here’s a brief comparison of the two:

Syntax: Go has a simpler syntax than C++. It uses indentation for block structure and has fewer keywords and symbols. C++ has a more complex syntax with a lot of features that can make it harder to learn and use effectively.

Memory Management: C++ gives the programmer more control over memory management through its support for pointers, manual memory allocation, and deallocation. Go, on the other hand, uses a garbage collector to automatically manage memory, making it less error-prone.

Concurrency: Go has built-in support for concurrency through goroutines and channels, which make it easier to write concurrent code. C++ has a thread library that can be used to write concurrent code, but it requires more manual management of threads and locks.

Performance: C++ is often considered a high-performance language, and it can be used for system-level programming and performance-critical applications. Go is also fast but may not be as fast as C++ in some cases.

Libraries and Frameworks: C++ has a vast ecosystem of libraries and frameworks that can be used for a variety of applications, from game development to machine learning. Go’s ecosystem is smaller, but it has good support for web development and distributed systems.

Overall, the choice of programming language depends on the project requirements, the available resources, and the developer’s expertise. Both Go and C++ have their strengths and weaknesses, and the best choice depends on the specific needs of the project.

Go语法介绍

string/[]byte

  • string是golang的基本数组类型,s := “hello,world”,一旦初始化后不允许修改其内容
  • 内部实现结构,指向数据的指针data和表示长度的len
  • 字符串拼接和格式化四种方式,+=,strings.join,buffer.writestring,fmt.sprintf
  • string 与 []byte的类型转换
  • 标准库strings提供了许多字符串操作的函数,例如Split、HasPrefix,Trim。

array

  • 数组array: [3]int{1,2,3}
  • 数组是值类型,数组传参发生拷贝
  • 定长
  • 数组的创建、初始化、访问和遍历range,len(arr)求数组的长度

slice

  • 切片slice初始化: make([]int,len,cap)
  • slice是引用类型
  • 变长,用容量和长度的区别,分别使用cap和len函数获取
  • 内存结构和实现:指针、cap、size共24字节
  • 常用函数,append,cap,len
  • 切片动态扩容
  • 深拷贝copy和浅拷贝“=”的区别
  • copy(slice1,slice2)

map

sync.map

struct

  • 空结构体struct{}的用途,节省内存。
  • 不支持继承,使用结构体嵌套组合
  • struct 可以比较吗?普通struct可以比较,带引用的struc不可比较,需要使用reflect.DeepEqual
  • struct没有slice和map类型时可直接判断
  • slice和map本身不可比较,需要使用reflect.DeepEqual()。
  • struct中包含slice和map等字段时,也要使用reflect.DeepEqual().
  • https://stackoverflow.com/questions/24534072/how-to-compare-struct-slice-map-are-equal

interface

  • https://draveness.me/golang/docs/part2-foundation/ch04-basic/golang-interface/
  • 隐式接口,实现接口的所有方法就隐式地实现了接口;不需要显示申明实现某接口
  • 接口也是 Go 语言中的一种类型,它能够出现在变量的定义、函数的入参和返回值中并对它们进行约束,不过 Go 语言中有两种略微不同的接口,一种是带有一组方法的接口,另一种是不带任何方法的 interface{}:
  • interface{} 类型不是任意类型,而是将类型转换成了 interface{} 类型
  • 结构体实现接口 vs 结构体指针实现接口 区别?
  • runtime.eface 和 runtime.iface 结构?
  • 结构体类型转化为接口的类型相互变换,interface类型断言为struct类型 过程
  • 动态派发与多态。动态派发(Dynamic dispatch)是在运行期间选择具体多态操作(方法或者函数)执行的过程,它是面向对象语言中的常见特性6。Go 语言虽然不是严格意义上的面向对象语言,但是接口的引入为它带来了动态派发这一特性,调用接口类型的方法时,如果编译期间不能确认接口的类型,Go 语言会在运行期间决定具体调用该方法的哪个实现。
  • Golang没有泛型,通过interface可以实现简单泛型编程,例如的sort的实现
  • 接口实现的源码

channel

  • Go鼓励CSP模型(communicating sequential processes),Goroutin之间通过channel传递数据
  • 非缓冲的同步channel和带缓冲的异步channel
  • 内部实现结构,带锁的循环队列runtime.hchan
  • channel创建make
  • chan <- i
  • 向channel发送数据。在发送数据的逻辑执行之前会先为当前 Channel 加锁,防止多个线程并发修改数据。如果 Channel 已经关闭,那么向该 Channel 发送数据时会报 “send on closed channel” 错误并中止程序。分为的三个部分:
    当存在等待的接收者时,通过 runtime.send 直接将数据发送给阻塞的接收者;
    当缓冲区存在空余空间时,将发送的数据写入 Channel 的缓冲区;
    当不存在缓冲区或者缓冲区已满时,等待其他 Goroutine 从 Channel 接收数据;
  • i <- ch,i, ok <- ch
  • 从channel接收数据的五种情况:
    • 如果 Channel 为空,那么会直接调用 runtime.gopark 挂起当前 Goroutine;
    • 如果 Channel 已经关闭并且缓冲区没有任何数据,runtime.chanrecv 会直接返回;
    • 如果 Channel 的 sendq 队列中存在挂起的 Goroutine,会将 recvx 索引所在的数据拷贝到接收变量所在的内存空间上并将 sendq 队列中 Goroutine 的数据拷贝到缓冲区;
    • 如果 Channel 的缓冲区中包含数据,那么直接读取 recvx 索引对应的数据;
    • 在默认情况下会挂起当前的 Goroutine,将 runtime.sudog 结构加入 recvq 队列并陷入休眠等待调度器的唤醒;
  • 关闭channel
  • 如何优雅的关闭channel?https://www.jianshu.com/p/d24dfbb33781, channel关闭后读操作会发生什么?写操作会发生什么?

类型和拷贝方式

  • 值类型 :String,Array,Int,Struct,Float,Bool,pointer(深拷贝)
  • 引用类型:Slice,Map (浅拷贝)

函数和方法,匿名函数

  • init函数
  • 值接收和指针接收的区别
  • 匿名函数?闭包?闭包延时绑定问题?用闭包写fibonacci数列?

指针和unsafe.Pointer

  • 相比C/C++,为了安全性考虑,Go指针弱化。不同类型的指针不能相互转化,指针变量不支持运算,不支持c/c++中的++,需要借助unsafe包
  • 任何类型的指针都可以被转换成unsafe.Pointer类型,通过unsafe.Pointer实现不同类型指针的转化
  • uintptr值可以被转换成unsafe.Pointer类型,通过uintptr实现指针的运算
  • unsafe.Pointer是一个指针类型,指向的值不能被解析,类似于C/C++里面的(void *),只说明这是一个指针,但是指向什么的不知道。
  • uintptr 是一个整数类型,这个整数的宽度足以用来存储一个指针类型数据;那既然是整数类类型,当然就可以对其进行运算了
  • nil
  • 实践string和[]byte的高效转换
  • 在业务场景中,使用指针虽然方便,但是要注意深拷贝和浅拷贝,这种错误还是比较常见的
  • 当你对象是结构体对象的指针时,你想要获取字段属性时,可以直接使用’.’,而不需要解引用

集合set

  1. golang中本身没有提供set,但可以通过map自己实现
  2. 利用map键值不可重复的特性实现set,value为空结构体。 map[interface{}]struct{}
  3. 如何自己实现set?

defer

  • defer定义的延迟函数参数在defer语句出时就已经确定下来了
  • defer定义顺序与实际执行顺序相反
  • return不是原子操作,执行过程是: 保存返回值(若有)–>执行defer(若有)–>执行ret跳转
  • 申请资源后立即使用defer关闭资源是好习惯
  • golang中的defer用途?调用时机?调用顺序?预计算值?
  • defer 实现原理?

Go 错误处理 error、panic

  • 在Go 语言中,错误被认为是一种可以预期的结果;而异常则是一种非预期的结果,发生异常可能表示程序中存在BUG 或发生了其它不可控的问题。
  • Go 语言推荐使用 recover 函数将内部异常转为错误处理,这使得用户可以真正的关心业务相关的错误处理。
  • 在Go服务中通常需要自定义粗错误类型,最好能有效区分业务逻辑错误和系统错误,同时需要捕获panic,将panic转化为error,避免某个错误影响server重启
  • panic 时需要保留runtime stack
    1
    2
    3
    4
    5
    6
    7
    8
    9
     defer func() {
    if x := recover(); x != nil {
    panicReason := fmt.Sprintf("I'm panic because of: %v\n", x)
    logger.LogError(panicReason)
    stk := make([]byte, 10240)
    stkLen := runtime.Stack(stk, false)
    logger.LogErrorf("%s\n", string(stk[:stkLen]))
    }
    }()

Go channel通道

channel

channel是golang中的csp并发模型非常重要组成部分,使用起来非常像阻塞队列。

  • 通道channel变量本身就是指针,可用“==”操作符判断是否为同一对象
  • 未初始化的channel为nil,需要使用make初始化
  • 理解初始化的channel和nil channel的区别?读写nil channel都会阻塞,关闭nil channel会出现panic;可以读关闭的channel,写关闭的channel会发出panic,close关闭了的channel会发出panic
  • 同步模式的channel必须有配对操作的goroutine出现,否则会一直阻塞,而异步模式在缓冲区未满或者数据未读完前,不会阻塞。
  • 内置的cap和len函数返回channel缓冲区大小和当前已缓冲的数量,而对于同步通道则返回0
  • 除了使用”<-“发送和接收操作符外,还可以用ok-idom或者range模式处理chanel中的数据。
  • 重复关闭和关闭nil channel都会导致pannic
  • make可以创建单项通道,但那没有意义,通产使用类型转换来获取单向通道,并分别赋予给操作方
  • 无法将单向通道转换成双向通道

基本用法

  1. 协程之间传递数据
  2. 用作事件通知,经常使用空结构体channel作为某个事件通知
  3. select帮助同时多个通道channel,它会随机选择一个可用的通道做收发操作
  4. 使用异步channel(带有缓冲)实现信号量semaphore
  5. 标准库提供了timeout和tick的channel实现。
  6. 通道并非用来取代锁的,通道和锁有各自不同的使用场景,通道倾向于解决逻辑层次的并发处理架构,而锁则用来保护数据的安全性。
  7. channel队列本质上还是使用锁同步机制,单次获取更多的数据(批处理),减少收发的次数,可改善因为频繁加锁造成的性能问题。
  8. channel可能会导致goroutine leak问题,是指goroutine处于发送或者接收阻塞状态,但一直未被唤醒,垃圾回收器并不收集此类资源,造成资源的泄露。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    func main() {
    done := make(chan struct{})
    s := make(chan int)
    go func() {
    s <- 1
    close(done)
    }()
    fmt.Println(<-s)
    <-done
    }

    func main() {
    sem := make(chan struct{}, 2) //two groutine
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
    wg.Add(1)
    go func(id int) {
    defer wg.Done()
    defer func() { <-sem }()
    sem <- struct{}{}
    time.Sleep(1 * time.Second)
    fmt.Println("id=", id)
    }(i)
    }
    wg.Wait()
    }


    func main() {
    go func() {
    tick := time.Tick(1 * time.Second)
    for {
    select {
    case <-time.After(5 * time.Second):
    fmt.Println("time out")
    case <-tick:
    fmt.Println("time tick 1s")
    default:
    fmt.Println("default")
    }
    }
    }()
    <-(chan struct{})(nil)
    }

Go并发模型 (Goroutine/channel/GMP)

what’s CSP?

The Communicating Sequential Processes (CSP) model is a theoretical model of concurrent programming that was first introduced by Tony Hoare in 1978. The CSP model is based on the idea of concurrent processes that communicate with each other by sending and receiving messages through channels.The Go programming language provides support for the CSP model through its built-in concurrency features, such as goroutines and channels. In Go, concurrent processes are represented by goroutines, which are lightweight threads of execution. The communication between goroutines is achieved through channels, which provide a mechanism for passing values between goroutines in a safe and synchronized manner.

Which is Goroutine ?

  • Goroutines are lightweight, user-level threads of execution that run concurrently with other goroutines within the same process.
  • Unlike traditional threads, goroutines are managed by the Go runtime, which automatically schedules and balances their execution across multiple CPUs and makes efficient use of available system resources.

比较Goroutine、thread、process

  • 比较进程、线程和Goroutine。进程是资源分配的单位,有独立的地址空间,线程是操作系统调度的单位,协程是更细力度的执行单元,需要程序自身调度。Go语言原生支持Goroutine,并提供高效的协程调度模型。
  • Goroutines, threads, and processes are all mechanisms for writing concurrent and parallel code, but they have some important differences:
  • Goroutines: A goroutine is a lightweight, user-level thread of execution that runs concurrently with other goroutines within the same process. Goroutines are managed by the Go runtime, which automatically schedules and balances their execution across multiple CPUs. Goroutines require much less memory and have much lower overhead compared to threads, allowing for many goroutines to run simultaneously within a single process.
  • Threads: A thread is a basic unit of execution within a process. Threads are independent units of execution that share the same address space as the process that created them. This allows threads to share data and communicate with each other, but also introduces the need for explicit synchronization to prevent race conditions and other synchronization issues.
  • Processes: A process is a self-contained execution environment that runs in its own address space. Processes are independent of each other, meaning that they do not share memory or other resources. Communication between processes requires inter-process communication mechanisms, such as pipes, sockets, or message queues.
  • In general, goroutines provide a more flexible and scalable approach to writing concurrent code compared to threads, as they are much lighter and more efficient, and allow for many more concurrent units of execution within a single process. Processes provide a more secure and isolated execution environment, but have higher overhead and require more explicit communication mechanisms.

Why is Goroutine lighter and more efficient than thread or process?

  • Stack size: Goroutines have a much smaller stack size compared to threads. The stack size of a goroutine is dynamically adjusted by the Go runtime, based on the needs of the goroutine. This allows for many more goroutines to exist simultaneously within a single process, as they require much less memory.
  • Scheduling: Goroutines are scheduled by the Go runtime, which automatically balances and schedules their execution across multiple CPUs. This eliminates the need for explicit thread management and synchronization, reducing overhead.
  • Context switching: Context switching is the process of saving and restoring the state of a running thread in order to switch to a different thread. Goroutines have a much lower overhead for context switching compared to threads, as they are much lighter and require less state to be saved and restored.
  • Resource sharing: Goroutines share resources with each other and with the underlying process, eliminating the need for explicit resource allocation and deallocation. This reduces overhead and allows for more efficient use of system resources.
  • Overall, the combination of a small stack size, efficient scheduling, low overhead context switching, and efficient resource sharing makes goroutines much lighter and more efficient than threads or processes, and allows for many more concurrent units of execution within a single process.
  • Goroutine 上下文切换只涉及到三个寄存器(PC / SP / DX)的值修改;而对比线程的上下文切换则需要涉及模式切换(从用户态切换到内核态)、以及 16 个寄存器、PC、SP…等寄存器的刷新;内存占用少:线程栈空间通常是 2M,Goroutine 栈空间最小 2K;Golang 程序中可以轻松支持10w 级别的 Goroutine 运行,而线程数量达到 1k 时,内存占用就已经达到 2G。
  • 理解G、P、M的含义以及调度模型

How are goroutines scheduled by runtime?

  • Cooperative (协作式). The scheduler uses a cooperative scheduling model, which means that goroutines voluntarily yield control to the runtime when they are blocked or waiting for an event.
  • Timer-based preemption. The scheduler uses a technique called timer-based preemption to interrupt the execution of a running goroutine and switch to another goroutine if it exceeds its time slice
  • Work-stealing. The scheduler uses a work-stealing algorithm, where each CPU has its own local run queue, and goroutines are dynamically moved between run queues to balance the o balance the load and improve performance.
  • no explicit prioritization. The Go runtime scheduler does not provide explicit support for prioritizing goroutines. Instead, it relies on the cooperative nature of goroutines to ensure that all goroutines make progress. In a well-designed Go program, the program should be designed such that all goroutines make progress in a fair and balanced manner.
  • https://blog.csdn.net/sinat_34715587/article/details/124990458
  • G 的数量可以远远大于 M 的数量,换句话说,Go 程序可以利用少量的内核级线程来支撑大量 Goroutine 的并发。多个 Goroutine 通过用户级别的上下文切换来共享内核线程 M 的计算资源,但对于操作系统来说并没有线程上下文切换产生的性能损耗,支持任务窃取(work-stealing)策略:为了提高 Go 并行处理能力,调高整体处理效率,当每个 P 之间的 G 任务不均衡时,调度器允许从 GRQ,或者其他 P 的 LRQ 中获取 G 执行。
  • 减少因Goroutine创建大量M:
    • 由于原子、互斥量或通道操作调用导致 Goroutine 阻塞,调度器将把当前阻塞的 Goroutine 切换出去,重新调度 LRQ 上的其他 Goroutine;
    • 由于网络请求和 IO 操作导致 Goroutine 阻塞,通过使用 NetPoller 进行网络系统调用,调度器可以防止 Goroutine 在进行这些系统调用时阻塞 M。这可以让 M 执行 P 的 LRQ 中其他的 Goroutines,而不需要创建新的 M。有助于减少操作系统上的调度负载。
    • 当调用一些系统方法的时候,如果系统方法调用的时候发生阻塞,这种情况下,网络轮询器(NetPoller)无法使用,而进行系统调用的 Goroutine 将阻塞当前 M,则创建新的M。阻塞的系统调用完成后:M1 将被放在旁边以备将来重复使用
    • 如果在 Goroutine 去执行一个 sleep 操作,导致 M 被阻塞了。Go 程序后台有一个监控线程 sysmon,它监控那些长时间运行的 G 任务然后设置可以强占的标识符,别的 Goroutine 就可以抢先进来执行。

What are the states of Goroutine and how do they flow?

  • 协程的状态流转?Grunnable、Grunning、Gwaiting
  • In Go, a Goroutine can be in one of several states during its lifetime. The states are:
  • New: The Goroutine is created but has not started executing yet.
  • Running: The Goroutine is executing on a machine-level thread.
  • Waiting: The Goroutine is waiting for some external event, such as I/O, channel communication, or a timer.
  • Sleeping: The Goroutine is sleeping, or waiting for a specified amount of time.
  • Dead: The Goroutine has completed its execution and is no longer running.

In summary, the lifetime of a Goroutine in Go starts when it is created and ends when it completes its execution or encounters a panic, and can be influenced by synchronization mechanisms such as channels and wait groups.

  • Golang context 用于在树形goroutine结构中,通过信号减少资源的消耗,包含Deadline、Done、Error、Value四个接口
  • 常用的同步原语:channel、sync.mutex、sync.RWmutex、sync.WaitGroup、sync.Once、atomic
  • 协程的状态流转?Grunnable、Grunning、Gwaiting
  • sync.Mutex 和 sync.RWMutex 互斥锁和读写锁的使用场景?
  • sync.Mutex: “锁”实现背后那些事
  • Golang 协程优雅的退出?
  • 深入理解协程gmp调度模型,以及其发展历史
  • 理解操作系统是怎么调度的,golang协程调度的优势,切换代价低,goroutine开销低,并发度高。
  • Golang IO 模型和网络轮训器

Go 内存管理和垃圾回收(memory and gc)

内存管理基本策略

为了兼顾内存分配的速度和内存利用率,大多数都采用以下策略进行内存管理:

  1. 申请:每次从操作系统申请一大块内存(比如1MB),以减少系统调用
  2. 切分:为了兼顾大小不同的对象,将申请到的内存按照一定的策略切分成小块,使用链接相连
  3. 分配:为对象分配内存时,只需从大小合适的链表中提取一块即可。
  4. 回收复用: 对象不再使用时,将该小块内存归还到原链表
  5. 释放: 如果闲置内存过多,则尝试归凡部分内存给操作系统,减少内存开销。

golang内存管理

 golang内存管理基本继承了tcmolloc成熟的架构,因此也符合内存管理的基本策略。

  1. 分三级管理,线程级的thread cache,中央center cache,和管理span的center heap。
  2. 每一级都采用链表管理不同size空闲内存,提高内存利用率
  3. 线程级的tread local cache能够减少竞争和加锁操作,提高效率。中央center cache为所有线程共享。
  4. 小对象直接从本地cache获取,大对象从center heap获取,提高内存利用率
  5. 每一级内存不足时,尝试从下一级内存获取
    内存三级管理
    线程cache
    大对象span管理
  • 多级缓存:内存分配器不仅会区别对待大小不同的对象,还会将内存分成不同的级别分别管理,TCMalloc 和 Go 运行时分配器都会引入线程缓存(Thread Cache)、中心缓存(Central Cache)和页堆(Page Heap)三个组件分级管理内存
  • 对象大小:Go 语言的内存分配器会根据申请分配的内存大小选择不同的处理逻辑,运行时根据对象的大小将对象分成微对象、小对象和大对象三种,tiny,small,large
  • mspan、mcache、mcentral、mheap

What are the memory leak scenarios in Go language?

  • Goroutine leaks: If a goroutine is created and never terminated, it can result in a memory leak. This can occur when a program creates a goroutine to perform a task but fails to provide a mechanism for the goroutine to terminate, such as a channel to receive a signal to stop.

  • Leaked closures: Closures are anonymous functions that capture variables from their surrounding scope. If a closure is created and assigned to a global variable, it can result in a memory leak, as the closure will continue to hold onto the captured variables even after they are no longer needed.

  • Incorrect use of channels: Channels are a mechanism for communicating between goroutines. If a program creates a channel but never closes it, it can result in a memory leak. Additionally, if a program receives values from a channel but never discards them, they will accumulate in memory and result in a leak.

  • Unclosed resources: In Go, it’s important to close resources, such as files and network connections, when they are no longer needed. Failure to do so can result in a memory leak, as the resources and their associated memory will continue to be held by the program.

  • Unreferenced objects: In Go, unreferenced objects are objects that are no longer being used by the program but still exist in memory. This can occur when an object is created and never explicitly deleted or when an object is assigned a new value and the old object is not properly disposed of.
    By following best practices and being mindful of these common scenarios, you can help to avoid memory leaks in your Go programs. Additionally, you can use tools such as the Go runtime profiler to detect and diagnose memory leaks in your programs.

  • Memory Leaking Scenarios

    • hanging goroutine
    • cgo
    • substring/slice
    • ticker

golang支持垃圾回收,gc能减少编程的负担,但与此同时也可能造成程序的性能问题。那么如何测量golang程序使用的内存,以及如何减少golang gc的负担呢?经历了许多版本的迭代,golang gc 沿着低延迟和高吞吐的目标在进化,相比早起版本,目前有了很大的改善,但仍然有可能是程序的瓶颈。因此要学会分析golang 程序的内存和垃圾回收问题。

如何查看程序的gc信息?

  1. 通过设置环境变量?env GODEBUG=gctrace=1
    例如: env GODEBUG=gctrace=1 godoc -http=:8080
  2. import _ “net/http/pprof”,查看/debug/pprof

tips:

  1. 减少内存分配,优先使用第二种APIs
    func (r *Reader) Read() ([]byte, error)
    func (r *Reader) Read(buf []byte) (int, error)
  2. 尽量避免string 和 []byte之间的转换
  3. 尽量减少两个字符串的合并
  4. 对slice预先分配大小
  5. 尽量不要使用cgo,因为c和go毕竟是两种语言。cgo是个high overhead的操作,调用cgo相当于阻塞IO,消耗一个线程
  6. defer is expensive?在性能要求较高的时候,考虑少用
  7. 对IO操作设置超时机制是个好习惯SetDeadline, SetReadDeadline, SetWriteDeadline
  8. 当数据量很大的时候,考虑使用流式IO(streaming IO)。io.ReaderFrom / io.WriterTo

gc 的过程

  • Marking phase: In this phase, the Go runtime identifies all objects that are accessible by the program and marks them as reachable. Objects that are not marked as reachable are considered unreachable and eligible for collection.
  • Sweeping phase: In this phase, the Go runtime scans the memory heap and frees all objects that are marked as unreachable. The memory space occupied by these objects is now available for future allocation.
  • Compacting phase: In this phase, the Go runtime rearranges the remaining objects on the heap to reduce fragmentation and minimize the impact of future allocations and deallocations.

垃圾回收算法概述

  golang是近几年出现的带有垃圾回收的现代语言,其垃圾回收算法自然也相互借鉴。因此在学习golang gc之前有必要了解目前主流的垃圾回收方法。

  1. 引用计数:熟悉C++智能指针应该了解引用计数方法。它对每一个分配的对象增加一个计数的域,当对象被创建时其值为1。每次有指针指向该对象时,其引用计数增加1,引用该对象的对象被析构时,其引用计数减1。当该对象的引用计数为0时,该对象也会被析构回收。引用对象对于C++这类没有垃圾回收器,对于便于对象管理的是不错的工具,但是维护引用计数会造成程序运行效率下降。
  2. 标记-清扫: 标记清扫是古老的垃圾回收算法,出现在70年代。通过指定每个内存阈值或者时间长度,垃圾回收器会挂起用户程序,也称为STW(stop the world)。垃圾回收器gc会对程序所涉及的所有对象进行一次遍历以确定哪些内存单元可以回收,因此分为标记(mark)和清扫(sweep),标记阶段标明哪些内存在使用不能回收,清扫阶段将不需要的内存单元释放回收。标记清扫法最大的问题是需要STW,当程序使用的内存较多时,其性能会比较差,延时较高。
  3. 三色标记法: 三色标记法是对标记清扫的改进,也是golang gc的主要算法,其最大的的优点是能够让部分gc和用户程序并发进行。它将对象分为白色、灰色和黑色:
    • 开始时所有的对象都是白色
    • 从根出发,将所有可到达对象标记为灰色,放入待处理队列
    • 从待处理队列中取出灰色对象,并将其引用的对象标记为灰色放入队列中,其自身标记为黑色。
    • 重复步骤3,直到灰色对象队列为空。最终只剩下白色对象和黑色对象,对白色对象尽心gc。
  4. 另外,还有一些在此基础上进行优化改进的gc算法,例如分代收集,节点复制等,它会考虑到对象的生命周期的长度,减少扫描标记的操作,相对来说效率会高一些。

golang垃圾回收

  golang gc是使用三色标记清理法,为了对用户对象进行标记需要将用户程序所有线程全部冻结(STW),当程序中包含很多对象时,暂停时间会很长,用户逻辑对用户的反应就会中止。那么如何缩短这个过程呢?一种自然的想法,在三色标记法扫描之后,只会存在黑色和白色两种对象,黑色是程序正在使用的对象不可回收,白色对象是此时不会被程序的对象,也是gc的要清理的对象。那么回收白色对象肯定不会和用户程序造成竞争冲突,因此回收操作和用户程序是可以并发的,这样可以缩短STW的时间。

  写屏障使得扫描操作和回收操作都可以和用户程序并发。我们试想一下,刚把一个对象标记为白色,用户程序突然又引用了它,这种扫描操作就比较麻烦,于是引入了屏障技术。内存扫描和用户逻辑也可以并发执行,用户新建的对象认为是黑色的,已经扫描过的对象有可能因为用户逻辑造成对象状态发生改变。所以**对扫描过后的对象使用操作系统写屏障功能用来监控用户逻辑这段内存,一旦这段内存发生变化写屏障会发生一个信号,gc捕获到这个信号会重新扫描改对象,查看它的引用或者被引用是否发生改变,从而判断该对象是否应该被清理。因此通过写屏障技术,是的扫描操作也可以合用户程序并发执行。

  gc控制器:gc算法并不万能的,针对不同的场景可能需要适当的设置。例如大数据密集计算可能不在乎内存使用量,甚至可以将gc关闭。golang 通过百分比来控制gc触发的时机,设置的百分比指的是程序新分配的内存与上一次gc之后剩余的内存量,例如上次gc之后程序占有2MB,那么下一次gc触发的时机是程序又新分配了2MB的内存。我们可以通过SetGCPercent函数动态设置,默认值为100,当百分比设置为负数时例如-1,表明关闭gc。
SetGCPercent

golang gc调优实例

gc 是golang程序性能优化非常重要的一部分,建议依照下面两个实例实践golang程序优化。

 

What’s Go closure?

In Go, a closure is a function that has access to variables from its outer (enclosing) function’s scope. The closure “closes over” the variables, meaning that it retains access to them even after the outer function has returned. This makes closures a powerful tool for encapsulating data and functionality and for creating reusable code.

Encapsulating State

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import "fmt"

func counter() func() int {
i := 0
return func() int {
i++
return i
}
}

func main() {
c := counter()

fmt.Println(c()) // Output: 1
fmt.Println(c()) // Output: 2
fmt.Println(c()) // Output: 3
}

Implementing Callbacks

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import "fmt"

func forEach(numbers []int, callback func(int)) {
for _, n := range numbers {
callback(n)
}
}

func main() {
numbers := []int{1, 2, 3, 4, 5}

// Define a callback function to apply to each element of the numbers slice.
callback := func(n int) {
fmt.Println(n * 2)
}

// Use the forEach function to apply the callback function to each element of the numbers slice.
forEach(numbers, callback)
}

Fibonacci

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main

import "fmt"

func memoize(f func(int) int) func(int) int {
cache := make(map[int]int)
return func(n int) int {
if val, ok := cache[n]; ok {
return val
}
result := f(n)
cache[n] = result
return result
}
}

func fibonacci(n int) int {
if n <= 1 {
return n
}
return fibonacci(n-1) + fibonacci(n-2)
}

func main() {
fib := memoize(fibonacci)
for i := 0; i < 10; i++ {
fmt.Println(fib(i))
}
}

Factorial

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import "fmt"

func main() {
factorial := func(n int) int {
if n <= 1 {
return 1
}
return n * factorial(n-1)
}

fmt.Println(factorial(5)) // Output: 120
}

Event Handling

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package main

import (
"fmt"
"time"
)

type Button struct {
onClick func()
}

func NewButton() *Button {
return &Button{}
}

func (b *Button) SetOnClick(f func()) {
b.onClick = f
}

func (b *Button) Click() {
if b.onClick != nil {
b.onClick()
}
}

func main() {
button := NewButton()
button.SetOnClick(func() {
fmt.Println("Button Clicked!")
})

go func() {
for {
button.Click()
time.Sleep(1 * time.Second)
}
}()

fmt.Scanln()
}

Go http client 实践

最近在项目开发中使用http服务与第三方服务交互,感觉golang的http封装得很好,很方便使用但是也有一些坑需要注意,一是自动复用连接,二是Response.Body的读取和关闭

http客户端自动复用连接

首先用代码直观的体验http客户端自动复用连接特点
server.go

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "hello!")
    })
    http.ListenAndServe(":8848", nil)
}

client.go

func doReq() {
    resp, err := http.Get("http://127.0.0.1:8848/test")
    if err != nil {
        fmt.Println(err)
        return
    }
    io.Copy(os.Stdout, resp.Body)
    defer resp.Body.Close()
}

func main() {
    //http.DefaultTransport.(*http.Transport).MaxIdleConnsPerHost = 10
    for {
        go doReq()
        go doReq()
        //	go doReq()
        time.Sleep(300 * time.Millisecond)
    }
}

测试1:执行netstat | grep "8848" | wc -l 结果:一直都是4
测试2:增加一个go doReq(),继续测试,结果:是一直增大
测试3:在测试2的基础上设置MaxIdleConnsPerHost = 10,结果:一直都是6

测试1已经能说明golang的http会自动复用连接
测试2为什么连接数量会一直增加呢?原因是golang中默认只保持两条持久连接,http.Transport没有设置MaxIdleConnPerHost,于是便采用了默认的DefaultMaxIdleConnsPerHost,这个值是2。
测试3通过加大MaxIdleConnPerHost的值,就能高效的利用http的自动复用机制。

读取和关闭Response.Body

将Resonse.Body的读取的代码屏蔽,继续测试。

func doReq() {
    resp, err := http.Get("http://127.0.0.1:8848/test")
    if err != nil {
        fmt.Println(err)
        return
    }
    //io.Copy(os.Stdout, resp.Body)
    defer resp.Body.Close()
}  

测试结果发现,连接数一直增加。
产生的原因:body实际上是一个嵌套了多层的net.TCPConn,当body没有被完全读取,也没有被关闭是,那么这次的http事物就没有完成,除非连接因为超时终止了,否则相关资源无法被回收。
从实现上看只要body被读完,连接就能被回收,只有需要抛弃body时才需要close,似乎不关闭也可以。但那些正常情况能读完的body,即第一种情况,在出现错误时就不会被读完,即转为第二种情况。而分情况处理则增加了维护者的心智负担,所以始终close body是最佳选择。

Go sync.Pool

基本使用

https://golang.org/pkg/sync/
sync.Pool的使用非常简单,它具有以下几个特点:

  • sync.Pool设计目的是存放已经分配但暂时不用的对象,供以后使用,以减轻gc的代价,提高效率
  • 存储在Pool中的对象会随时被gc自动回收,Pool中对象的缓存期限为两次gc之间
  • 用户无法定义sync.Pool的大小,其大小仅仅受限于内存的大小
  • sync.Pool支持多协程之间共享

sync.Pool的使用非常简单,定义一个Pool对象池时,需要提供一个New函数,表示当池中没有对象时,如何生成对象。对象池Pool提供Get和Put函数从Pool中取和存放对象。

下面有一个简单的实例,直接运行是会打印两次“new an object”,注释掉runtime.GC(),发现只会调用一次New函数,表示实现了对象重用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
"fmt"
"runtime"
"sync"
)

func main() {
p := &sync.Pool{
New: func() interface{} {
fmt.Println("new an object")
return 0
},
}

a := p.Get().(int)
a = 100
p.Put(a)
runtime.GC()
b := p.Get().(int)
fmt.Println(a, b)
}

sync.Pool 如何支持多协程共享?

sync.Pool支持多协程共享,为了尽量减少竞争和加锁的操作,golang在设计的时候为每个P(核)都分配了一个子池,每个子池包含一个私有对象和共享列表。 私有对象只有对应的和核P能够访问,而共享列表是与其它P共享的。

在golang的GMP调度模型中,我们知道协程G最终会被调度到某个固定的核P上。当一个协程在执行Pool的get或者put方法时,首先对改核P上的子池进行操作,然后对其它核的子池进行操作。因为一个P同一时间只能执行一个goroutine,所以对私有对象存取操作是不需要加锁的,而共享列表是和其他P分享的,因此需要加锁操作。

一个协程希望从某个Pool中获取对象,它包含以下几个步骤:

  1. 判断协程所在的核P中的私有对象是否为空,如果非常则返回,并将改核P的私有对象置为空
  2. 如果协程所在的核P中的私有对象为空,就去改核P的共享列表中获取对象(需要加锁)
  3. 如果协程所在的核P中的共享列表为空,就去其它核的共享列表中获取对象(需要加锁)
  4. 如果所有的核的共享列表都为空,就会通过New函数产生一个新的对象

在sync.Pool的源码中,每个核P的子池的结构如下所示:

// Local per-P Pool appendix.
type poolLocalInternal struct {
    private interface{}   // Can be used only by the respective P.
    shared  []interface{} // Can be used by any P.
    Mutex                 // Protects shared.
}

更加细致的sync.Pool源码分析,可参考http://jack-nie.github.io/go/golang-sync-pool.html

为什么不使用sync.pool实现连接池?

刚开始接触到sync.pool时,很容易让人联想到连接池的概念,但是经过仔细分析后发现sync.pool并不是适合作为连接池,主要有以下两个原因:

  • 连接池的大小通常是固定且受限制的,而sync.Pool是无法控制缓存对象的数量,只受限于内存大小,不符合连接池的目标
  • sync.Pool对象缓存的期限在两次gc之间,这点也和连接池非常不符合

golang中连接池通常利用channel的缓存特性实现。当需要连接时,从channel中获取,如果池中没有连接时,将阻塞或者新建连接,新建连接的数量不能超过某个限制。

https://github.com/goctx/generic-pool基于channel提供了一个通用连接池的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
package pool

import (
"errors"
"io"
"sync"
"time"
)

var (
ErrInvalidConfig = errors.New("invalid pool config")
ErrPoolClosed = errors.New("pool closed")
)

type Poolable interface {
io.Closer
GetActiveTime() time.Time
}

type factory func() (Poolable, error)

type Pool interface {
Acquire() (Poolable, error) // 获取资源
Release(Poolable) error // 释放资源
Close(Poolable) error // 关闭资源
Shutdown() error // 关闭池
}

type GenericPool struct {
sync.Mutex
pool chan Poolable
maxOpen int // 池中最大资源数
numOpen int // 当前池中资源数
minOpen int // 池中最少资源数
closed bool // 池是否已关闭
maxLifetime time.Duration
factory factory // 创建连接的方法
}

func NewGenericPool(minOpen, maxOpen int, maxLifetime time.Duration, factory factory) (*GenericPool, error) {
if maxOpen <= 0 || minOpen > maxOpen {
return nil, ErrInvalidConfig
}
p := &GenericPool{
maxOpen: maxOpen,
minOpen: minOpen,
maxLifetime: maxLifetime,
factory: factory,
pool: make(chan Poolable, maxOpen),
}

for i := 0; i < minOpen; i++ {
closer, err := factory()
if err != nil {
continue
}
p.numOpen++
p.pool <- closer
}
return p, nil
}

func (p *GenericPool) Acquire() (Poolable, error) {
if p.closed {
return nil, ErrPoolClosed
}
for {
closer, err := p.getOrCreate()
if err != nil {
return nil, err
}
// 如果设置了超时且当前连接的活跃时间+超时时间早于现在,则当前连接已过期
if p.maxLifetime > 0 && closer.GetActiveTime().Add(time.Duration(p.maxLifetime)).Before(time.Now()) {
p.Close(closer)
continue
}
return closer, nil
}
}

func (p *GenericPool) getOrCreate() (Poolable, error) {
select {
case closer := <-p.pool:
return closer, nil
default:
}
p.Lock()
if p.numOpen >= p.maxOpen {
closer := <-p.pool
p.Unlock()
return closer, nil
}
// 新建连接
closer, err := p.factory()
if err != nil {
p.Unlock()
return nil, err
}
p.numOpen++
p.Unlock()
return closer, nil
}

// 释放单个资源到连接池
func (p *GenericPool) Release(closer Poolable) error {
if p.closed {
return ErrPoolClosed
}
p.Lock()
p.pool <- closer
p.Unlock()
return nil
}

// 关闭单个资源
func (p *GenericPool) Close(closer Poolable) error {
p.Lock()
closer.Close()
p.numOpen--
p.Unlock()
return nil
}

// 关闭连接池,释放所有资源
func (p *GenericPool) Shutdown() error {
if p.closed {
return ErrPoolClosed
}
p.Lock()
close(p.pool)
for closer := range p.pool {
closer.Close()
p.numOpen--
}
p.closed = true
p.Unlock()
return nil
}

Go指针和unsafe.pointer

  1. 不同类型的指针不能相互转化
  2. 指针变量不能进行运算,不支持c/c++中的++,–运算
  3. 任何类型的指针都可以被转换成unsafe.Pointer类型,反之也是
  4. uintptr值可以被转换成unsafe.Pointer类型,反之也是
  5. 对unsafe.Pointer和uintptr两种类型单独解释两句:
    • unsafe.Pointer是一个指针类型,指向的值不能被解析,类似于C/C++里面的(void *),只说明这是一个指针,但是指向什么的不知道。
    • uintptr 是一个整数类型,这个整数的宽度足以用来存储一个指针类型数据;那既然是整数类类型,当然就可以对其进行运算了
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      package main
      import (
      "fmt"
      "unsafe"
      )
      func main() {
      var ii [4]int = [4]int{11, 22, 33, 44}
      px := &ii[0]
      fmt.Println(&ii[0], px, *px)
      //compile error
      //pf32 := (*float32)(px)

      //compile error
      // px = px + 8
      // px++

      var pointer1 unsafe.Pointer = unsafe.Pointer(px)
      var pf32 *float32 = (*float32)(pointer1)

      var p2 uintptr = uintptr(pointer1)
      print(p2)
      p2 = p2 + 8
      var pointer2 unsafe.Pointer = unsafe.Pointer(p2)
      var pi32 *int = (*int)(pointer2)

      fmt.Println(*px, *pf32, *pi32)

      }

nil

引用类型声明而没有初始化赋值时,其值为nil。golang需要经常判断nil,防止出现panic错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
bool  -> false  
numbers -> 0
string-> ""

pointers -> nil
slices -> nil
maps -> nil
channels -> nil
functions -> nil
interfaces -> nil

package main

import (
"fmt"
)

type Person struct {
AgeYears int
Name string
Friends []Person
}

func main() {
var p Person
fmt.Printf("%v\n", p)

var slice1 []int
fmt.Println(slice1)
if slice1 == nil {
fmt.Println("slice1 is nil")
}
// fmt.Println(slice1[0]) panic

// var c chan int
// close(c) panic
}

编译器优化和逃逸分析

逃逸分析(Escape analysis)

golang在内存分配的时候没有堆(heap)和栈(stack)的区别,由编译器决定是否需要将对象逃逸到堆中。例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
	func Sum() int {
const count = 100
numbers := make([]int, count)
for i := range numbers {
numbers[i] = i + 1
}

var sum int
for _, i := range numbers {
sum += i
}
return sum
}

func main() {
answer := Sum()
fmt.Println(answer)
}
1
2
3
4
5
$ go build -gcflags=-m test_esc.go 
command-line-arguments
./test_esc.go:9:17: Sum make([]int, count) does not escape
./test_esc.go:23:13: answer escapes to heap
./test_esc.go:23:13: main ... argument does not escape

内敛(Inlining)

了解C/C++的应该知道内敛,golang编译器同样支持函数内敛,对于较短且重复调用的函数可以考虑使用内敛

Dead code elimination/Branch elimination

编译器会将代码中一些无用的分支进行优化,分支判断,提高效率。例如下面一段代码由于a和b是常量,编译器也可以推导出Max(a,b),因此最终F函数为空
1
2
3
4
5
6
7
8
9
10
11
12
13
func Max(a, b int) int {
if a > b {
return a
}
return b
}

func F() {
const a, b = 100, 20
if Max(a, b) == b {
panic(b)
}
}

常用的编译器选项: go build -gcflags=”-lN” xxx.go

  • “-S”,编译时查看汇编代码
  • “-l”,关闭内敛优化
  • “-m”,打印编译优化的细节
  • “-l -N”,关闭所有的优化

Go runtime 介绍

  为了避开直接通过系统调用分配内存而导致的性能开销,通常会通过预分配、内存池等操作自主管理内存。golang由运行时runtime管理内存,完成初始化、分配、回收和释放操作。目前主流的内存管理器有glibc和tcmolloc,tcmolloc由Google开发,具有更好的性能,兼顾内存分配的速度和内存利用率。golang也是使用类似tcmolloc的方法进行内存管理。建议参考下面链接学习tcmalloc的原理,其内存管理的方法也是golang内存分配的方法。另外一个原因,golang自主管理也是为了更好的配合垃圾回收。
【1】.https://zhuanlan.zhihu.com/p/29216091
【2】.http://goog-perftools.sourceforge.net/doc/tcmalloc.html

What is the Go runtime?

The Go runtime is a collection of software components that provide essential services for Go programs, including memory management, garbage collection, scheduling, and low-level system interaction. The runtime is responsible for managing the execution of Go programs and for providing a consistent, predictable environment for Go code to run in.

At a high level, the Go runtime is responsible for several core tasks:

  • Memory management: The runtime manages the allocation and deallocation of memory used by Go programs, including the stack, heap, and other data structures.
  • Garbage collection: The runtime automatically identifies and frees memory that is no longer needed by a program, preventing memory leaks and other related issues.
  • Scheduling: The runtime manages the scheduling of Goroutines, the lightweight threads used by Go programs, to ensure that they are executed efficiently and fairly.
  • Low-level system interaction: The runtime provides an interface for Go programs to interact with low-level system resources, including system calls, I/O operations, and other low-level functionality.

The Go runtime is an essential component of the Go programming language, and it is responsible for many of the language’s unique features and capabilities. By providing a consistent, efficient environment for Go code to run in, the runtime enables developers to write high-performance, scalable software that can run on a wide range of platforms and architectures.

程序启动流程

  在golang中,可执行文件的入口函数并不是我们写的main函数,编译器在编译go代码时会插入一段起引导作用的汇编代码,它引导程序进行命令行参数、运行时的初始化,例如内存分配器初始化、垃圾回收器初始化、协程调度器的初始化。golang引导初始化之后就会进入用户逻辑,因为存在特殊的init函数,main函数也不是程序最开始执行的函数。

  golang可执行程序由于运行时runtime的存在,其启动过程还是非常复杂的,这里通过gdb调试工具简单查看其启动流程:

  1. 找一个golang编译的可执行程序test,info file查看其入口地址:gdb test,info files
    (gdb) info files
    Symbols from “/home/terse/code/go/src/learn_golang/test_init/main”.
    Local exec file:
    /home/terse/code/go/src/learn_golang/test_init/main’,
    file type elf64-x86-64.
    Entry point: 0x452110
    …..
  2. 利用断点信息找到目标文件信息:
    (gdb) b *0x452110
    Breakpoint 1 at 0x452110: file /usr/local/go/src/runtime/rt0_linux_amd64.s, line 8.
  3. 依次找到对应的文件对应的行数,设置断点,调到指定的行,查看具体的内容:
    (gdb) b _rt0_amd64
    (gdb) b b runtime.rt0_go
    至此,由汇编代码针对特定平台实现的引导过程就全部完成了,后续的代码都是用Go实现的。分别实现命令行参数初始化,内存分配器初始化、垃圾回收器初始化、协程调度器的初始化等功能。
    1
    2
    3
    4
    5
    6
    7
    CALL	runtime·args(SB)
    CALL runtime·osinit(SB)
    CALL runtime·schedinit(SB)

    CALL runtime·newproc(SB)

    CALL runtime·mstart(SB)

特殊的init函数

  1. init函数先于main函数自动执行,不能被其他函数调用
  2. init函数没有输入参数、没有返回值
  3. 每个包可以含有多个同名的init函数,每个源文件也可以有多个同名的init函数
  4. 执行顺序 变量初始化 > init函数 > main函数。在复杂项目中,初始化的顺序如下:
    • 先初始化import包的变量,然后先初始化import的包中的init函数,,再初始化main包变量,最后执行main包的init函数
    • 从上到下初始化导入的包(执行init函数),遇到依赖关系,先初始化没有依赖的包
    • 从上到下初始化导入包中的变量,遇到依赖,先执行没有依赖的变量初始化
    • main包本身变量的初始化,main包本身的init函数
    • 同一个包中不同源文件的初始化是按照源文件名称的字典序

  

程序bootstrap过程

如上图所示,Go程序启动大致分为一下一个部分:

  • 参数处理,runtime·args(SB)
  • 操作系统初始化,runtime·osinit(SB)
  • 调度器初始化,runtime·schedinit(SB)
  • 运行runtime.main函数,装载用户main函数并运行,runtime.main()
    参数处理和osinit逻辑比较简单,代码也较少,这里主要记录下调度器初始化和runtime.main函数两个部分

runtime·schedinit

schedinit内容比较多,主要包含:

  • 栈初始化 stackinit()
  • 堆初始化 mallocinit()
  • gc初始化 gcinit()
  • 初始化resize allp []*p procresize()

stack

stackinit() 核心代码用于初始化全局的stackpool和stackLarge两个结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
var stackpool [_NumStackOrders]struct {
item stackpoolItem
_ [cpu.CacheLinePadSize - unsafe.Sizeof(stackpoolItem{})%cpu.CacheLinePadSize]byte
}

//go:notinheap
type stackpoolItem struct {
mu mutex
span mSpanList
}

// Global pool of large stack spans.
var stackLarge struct {
lock mutex
free [heapAddrBits - pageShift]mSpanList // free lists by log_2(s.npages)
}

func stackinit() {
if _StackCacheSize&_PageMask != 0 {
throw("cache size must be a multiple of page size")
}
for i := range stackpool {
stackpool[i].item.span.init()
lockInit(&stackpool[i].item.mu, lockRankStackpool)
}
for i := range stackLarge.free {
stackLarge.free[i].init()
lockInit(&stackLarge.lock, lockRankStackLarge)
}
}

newproc 需要一个初始的stack

1
2
3
4
5
6
if gp.stack.lo == 0 {
// Stack was deallocated in gfput or just above. Allocate a new one.
systemstack(func() {
gp.stack = stackalloc(startingStackSize)
})
gp.stackguard0 = gp.stack.lo + _StackGuard

goroutine 运行时需要把stack 地址传给m

runtime.main

内存分配和管理策略mallocgc

垃圾回收garbage collector

程序并发Goroutine调度

Go 可测试编程、单元测试和性能优化

  Golang非常注重工程化,提供了非常好用单元测试、性能测试(benchmark)和调优工具(pprof),它们对提高代码的质量和服务的性能非常有帮助。参考链接中通过一段http代码非常详细的介绍了golang程序优化的步骤和方便之处。实际工作中,我们很难每次都对代码都有那么高的要求,但是能使用一些工具对程序进行优化程序性能也是golang程序员必备的技能。
dave它通过几个case非常清晰的介绍了golang性能分析与优化的技术,非常值得学习。https://dave.cheney.net/high-performance-go-workshop/dotgo-paris.html

  • testing 标准库
  • go test 测试工具
  • go tool pprof 分析 profile数据

单元测试,测试正确性

  1. 为了测试某个文件中的某个函数的性能,在相同目录下定义xxx_test.go文件,使用go build命令编译程序时会忽略测试文件

  2. 在测试文件中定义测试某函数的代码,以TestXxxx方式命名,例如TestAdd

  3. 在相同目录下运行 go test -v 即可观察代码的测试结果

     func TestAdd(t *testing.T) {
         if add(1, 3) != 4 {
             t.FailNow()
         }
     }
    

性能测试,benchmark

  1. 单元测试,测试程序的正确性。benchmark 用户测试代码的效率,执行的时间
  2. benchmark测试以BenchMark开头,例如BenchmarkAdd
  3. 运行 go test -v -bench=. 程序会运行到一定的测试,直到有比较准备的测试结果
    func BenchmarkAdd(b *testing.B) {
    for i := 0; i < b.N; i++ {
    _ = add(1, 2)
    }
    }

    BenchmarkAdd-4 2000000000 0.26 ns/op

pprof性能分析

  1. 除了使用使用testing进行单元测试和benchanmark性能测试,golang能非常方便捕获或者监控程序运行状态数据,它包括cpu、内存、和阻塞等,并且非常的直观和易于分析。
  2. 有两种捕获方式: a、在测试时输出并保存相关数据;b、在运行阶段,在线采集,通过web接口获得实时数据。
  3. Benchamark时输出profile数据:go test -v -bench=. -memprofile=mem.out -cpuprofile=cpu.out
  4. 使用go tool pprof xxx.test mem.out 进行交互式查看,例如top5。同理,可以分析其它profile文件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
(pprof) top5
Showing nodes accounting for 1994.93MB, 63.62% of 3135.71MB total
Dropped 28 nodes (cum <= 15.68MB)
Showing top 5 nodes out of 46
flat flat% sum% cum cum%
475.10MB 15.15% 15.15% 475.10MB 15.15% regexp/syntax.(*compiler).inst
455.58MB 14.53% 29.68% 455.58MB 14.53% regexp.progMachine
421.55MB 13.44% 43.12% 421.55MB 13.44% regexp/syntax.(*parser).newRegexp
328.61MB 10.48% 53.60% 328.61MB 10.48% regexp.onePassCopy
314.09MB 10.02% 63.62% 314.09MB 10.02% net/http/httptest.cloneHeader

- flat:仅当前函数,不包括它调用的其它函数
- cum: 当前函数调用堆栈的累计
- sum: 列表前几行所占百分比的总和

实际操作

Go实践:Goroutine同步方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
package main

import (
"context"
"fmt"
"sync"
"time"
)

//sync package
func sync1() {
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1) //设置协程等待的个数
go func(x int) {
defer func() {
wg.Done()
}()
fmt.Println("I'm", x)
}(i)
}
wg.Wait()
}

//chan
func sync2() {
chanSync := make([]chan bool, 10)
for i := 0; i < 10; i++ {
chanSync[i] = make(chan bool)
go func(x int, ch chan bool) {
fmt.Println("I'm ", x)
ch <- true
}(i, chanSync[i])
}

for _, ch := range chanSync {
<-ch
}
}

//context
func sync3() {
ctx, cancelFunc := context.WithCancel(context.Background())
defer cancelFunc()

for i := 0; i < 10; i++ {
go func(ctx context.Context, i int) {
for {
select {
case <-ctx.Done():
fmt.Println(ctx.Err(), i)
return
case <-time.After(2 * time.Second):
fmt.Println("time out", i)
return
}
}
}(ctx, i)
}
time.Sleep(5 * time.Second)
}

func main() {
sync1()
sync2()
sync3()
time.Sleep(10 * time.Second)
}

Go实践:生产者、消费者模型,并行计算累加求和

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
package main

import (
"math/rand"
"sync"
"sync/atomic"

"fmt"
)

var total int32 = 100000

var producerLimit int32 = 3

var consumerLimit int32 = 4

var Q chan int32
var SumQ chan int32

var AtomicSum int32 = 0

func init() {
Q = make(chan int32, 10)
SumQ = make(chan int32)
}

func produce() {
a := total / producerLimit
b := total % producerLimit
var wg sync.WaitGroup
for i := 0; i < int(producerLimit); i++ {
batch := a
if i < int(b) {
batch += 1
}
wg.Add(1)
go func(x int32) {
defer wg.Done()
for j := 0; j < int(x); j++ {
num := rand.Intn(10)
atomic.AddInt32(&AtomicSum, int32(num))
Q <- int32(num)
}
}(batch)
}
go func() {
wg.Wait()
close(Q)
}()
}

func consumer() int32 {
var wg sync.WaitGroup
for i := 0; i < int(consumerLimit); i++ {
wg.Add(1)
go func() {
defer wg.Done()
var batchSum int32 = 0
for num := range Q {
batchSum += num
}
SumQ <- batchSum
}()
}

go func() {
wg.Wait()
close(SumQ)
}()

var ans int32 = 0
for sum := range SumQ {
ans += sum
}
return ans
}

func main() {
produce()
fmt.Printf("%d,%d\n", consumer(), atomic.LoadInt32(&AtomicSum))
}

Go 实践:interface/base/derive

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package main

import "fmt"

type service interface {
foo1()
foo2()
}

type baseService struct {
name string
}

func NewBaseService(name string) *baseService {
b := baseService{}
b.name = name
return &b
}

func (b *baseService) foo1() {
fmt.Println(b.name)
}

func (b *baseService) foo2() {
fmt.Println(b.name)
}

type AService struct {
*baseService
name string
}

func NewAService(name string, b *baseService) *AService {
s := AService{}
s.baseService = b
s.name = name
return &s
}

func (a *AService) foo1() {
fmt.Println(a.name)
}

func foo(s service) {
s.foo1()
s.foo2()
}

func main() {
b := NewBaseService("baseService")
s := NewAService("AService", b)
foo(s)
}

Go实践:设计模式的实现

https://refactoringguru.cn/design-patterns/chain-of-responsibility/go/example

Go 1.12 压测后rss内存一直无法释放问题

包和库(package)

参考

基础语法与关键字

const 关键字

  • 定义常量、指针、引用、对象,const进行修饰的变量的值在程序的任意位置将不能再被修改
  • 修饰参数: const int x
  • 修饰成员变量: const成员变量必须通过初始化列表进行初始化
  • 修饰成员函数
  • C++ mutable变量突破const修饰成员函数的限制

static 关键字

  • 静态全局变量
  • 修饰函数内部的局部变量,作用相当于全局变量
  • 类的静态成员变量
  • 类的静态成员函数

宏定义和内联函数

  • 内联函数和宏定义减少函数调用所带来的时间和空间的开销,以空间换时间的策略
  • 宏是在预编译阶段简单文本替代,inline在编译阶段实现展开
  • 宏肯定会被替代,而复杂的inline函数不会被展开
  • 宏容易出错(运算顺序),且难以被调试,inline不会
  • 宏不是类型安全,而inline是类型安全的
  • 当函数size太大,inline虚函数,函数中存在循环或递归,内联可能失效

extern 与 static

  • extern是C/C++语言中表明函数和全局变量作用范围(可见性)的关键字
  • 该关键字告诉编译器,其声明的函数和变量可以在本模块或其它模块中使用
  • 与extern对应的关键字是static,被它修饰的全局变量和函数只能在本模块使用

extern “C”

  • extern “C”是为了实现C和C++的混合编程
  • C和C++的编译和链接是不完全相同的
  • extern “C”表明它按照类C的编译和连接规约来编译和连接,而不是C++的编译和链接
  • C++是一个面向对象语言,它为了支持函数的重载,在编译的时候会带上参数的类型来唯一标识每个函数
  • C语言中并没有重载和类这些特性,故并不像C++那样print(int i)会被编译为_print_int

struct和class的区别

  • 在C++中struct和class的区别比较小,主要类成员的默认访问权限和继承权限
  • 默认继承权限:如果不明确指定,来自class的继承按照private继承处理,来自struct的继承按照public继承处理
  • 成员的默认访问权限:class的成员默认是private权限,struct默认是public权限
  • 仅当只有数据成员时使用struct,其它一概使用class(google编码规范)

类型安全

  • 静态类型 vs 动态类型: 静态类型(C/C++,java,golang),动态类型(python)
  • 弱类型 vs 强类型: 弱类型(C、C++),强类型(python、golang)
  • 类型安全: 一般来说弱类型存在隐含的类型转换都不是类型安全的,而强类型是类型安全的

C++中的四种类型转换

  • const_cast: 字面上理解就是去const属性
  • static_cast: 命名上理解是静态类型转换。如int转换成char。类似于C风格的强制转换
  • dynamic_cast: 命名上理解是动态类型转换。如子类和父类之间的多态类型转换
  • reinterpret_cast: 仅仅重新解释类型,但没有进行二进制的转换

volatile关键字

  • volatile指出变量是随时可能发生变化的
  • 每次使用它的时候必须从内存中读取
  • 编译器生成的汇编代码会重新从内存读取数据
  • 防止编译器优化

指针和引用

  • 指针指向一块内存,它的内容是所指内存的地址
  • 引用则是某块内存的别名,引用初始化后不能改变指向
  • 使用时,引用更加安全,指针更加灵活
  • 初始化:引用必须初始化,且初始化之后不能改变;指针可以不必初始化,且指针可以改变所指的对象
  • 空值:指针可以指向空值,不存在指向空值的引用
  • 引用和指针指向一个对象时,引用的创建和销毁不会调用类的拷贝构造函数和析构函数
  • 引用和指针与const:存在常量指针和常量引用指针,表示指向的对象是常量
  • 函数参数传递时使用指针或者引用的效果是相同的,都是简洁操作主调函数中的相关变量
  • sizeof引用的时候是对象的大小,sizeof指针是指针本身的大小

面向对象与类设计

C++的空类八个默认函数

  • 缺省构造函数
  • 拷贝构造函数
  • 赋值构造函数
  • 析构函数
  • 取值操作符函数
  • const取值操作符
  • 移动构造函数C++11
  • 移动赋值构造函数C++11

C++11中delete和default的作用

  • =default显式缺省,告知编译器生成函数默认的缺省版本
  • =delete显式删除,告知编译器不生成函数默认的缺省版本
  • C++11中引进这两种新特性的目的是为了增强对”类默认函数的控制”

C++11禁用隐式类型转换explicit

  • explicit关键字用来修饰类的构造函数
  • 被修饰的构造函数的类,不能发生相应的隐式类型转换
  • 只能以显示的方式进行类型转换

new/delete和malloc/free的使用

  • new/delete是C++的运算符,malloc/free是C/C++的库函数
  • new/delete和malloc/free必须配套使用
  • mallocl/free仅仅是在堆中分配内存,需要自己指定分配内存大小以及指针类型的转换
  • new/delete会根据对象的类型调用对应的构造函数和析构函数
  • new是类型安全的,而malloc不是

new/operator new和placement new

  • new:新建对象时用,是C++操作符
  • operator new就像operator + 一样,是可以重载的
  • placement new:只是operator new重载的一个版本
  • 它并不分配内存,只是返回指向已经分配好的某段内存的一个指针

sizeof

  • 空对象的大小为1个字节
  • 编译器内存对齐
  • 继承
  • 虚函数的影响

编译器内存对齐

  • 现代计算机中内存空间都是按照byte划分的
  • 实际的计算机系统对基本类型数据在内存中存放的位置有限制
  • 它们会要求这些数据的首地址的值是某个数k(通常它为4或8)的倍数
  • 这就是所谓的内存对齐
  • 编译器内存对齐是为了提高数据读写的效率

浅拷贝和深拷贝

  • 对于含有堆内存的对象,浅拷贝只是对指针的拷贝
  • 拷贝后两个指针指向同一个内存空间
  • 深拷贝对指针所指向的内容进行拷贝
  • 默认拷贝构造函数为浅拷贝

friend友元函数和友元类

  • 友元的作用是提高了程序的运行效率
  • 但它破坏了类的封装性和隐藏性
  • 使得非成员函数可以访问类的私有成员
  • 实际中这一特性很少使用

类的初始化列表

  • 初始化列表是C++11中新增的类成员初始化方式
  • 没有默认构造函数的类自定义类型成员必须使用初始化列表
  • const成员、引用类型成员必须使用初始化列表
  • 初始化列表中初始化初始化的顺序与成员定义的顺序相同,与初始化列表的顺序无关
  • 初始化列表的优点:主要是对于自定义类型,初始化列表是作用在函数体之前

重载、覆盖和重写

  • 重载(overload):同类中同名函数,参数的类型、个数或者返回类型不同
  • 覆盖(override):基类函数virtual函数,派生类中重写该函数
  • 重写(overwrite):派生类的函数屏蔽了与其同名的基类函数

继承/多继承/虚继承

  • 单继承、多继承,继承时构造函数和析构函数的调用顺序
  • 继承方式,public/protected/private,默认为private继承
  • 友元函数不能被继承
  • 静态成员和静态成员函数是可以继承的
  • 虚继承的概念
  • C++对象内存模型

virtual虚

  • 虚基类成员函数,派生类override这个虚函数
  • 虚析构函数
  • 虚函数的实现,虚函数表和虚函数指针
  • 虚函数的动态绑定机制与运行期多态
  • 虚继承,在多重继承关系中,为了避免菱形继承导致的资源浪费
  • 虚继承的实现,虚基表
  • 内敛函数不能为虚函数
  • 静态函数不能为虚函数
  • 构造函数不能为虚函数
  • 纯虚函数,类似于的接口的作用

C++对象模型

  • 对象的内存布局
  • 虚函数表
  • 虚基表
  • 继承关系中的内存布局

C++11中的移动语义

  • C++中的拷贝语义和移动语义
  • 右值引用和移动语义
  • 对含堆内存类的临时对象的拷贝和赋值函数的优化
  • 使的深拷贝转化为浅拷贝

C++11智能指针

  • unique_ptr
  • shared_ptr
  • weak_ptr
  • 智能指针的使用场景

模板编程

  • 模板,函数模板,类模板
  • C++类模板碰到static
  • 每个类型一个static值
  • C++类中不能包含虚函数模板
  • 类模板可以包含虚函数
  • 模板的声明和实现为何要放在头文件中
  • 模板元编程

访函数/函数指针/lamda表达式

  • 函数对象(function object)又叫仿函数(functor)
  • 就是重载了调用运算符()的类,所生成的对象
  • 函数指针也是一个函数对象
  • lamda表达式

C++异常处理

  • 返回错误码
  • 断言
  • 异常处理Exception
  • 构造函数可以抛异常,析构函数不能抛异常

RTTI

  • RTTI是”Runtime Type Information”的缩写
  • 意思是运行时类型信息
  • 它提供了运行时确定对象类型的方法
  • 实现机制是虚函数和虚函数表
  • 谷歌禁用使用RTTI

前置声明(forward declaration)

  • 所谓「前置声明」(forward declaration)是类、函数和模板的纯粹声明
  • 没伴随着其定义
  • 尽可能地避免使用前置声明
  • 使用#include包含需要的头文件即可

#include头文件的顺序

  • C系统文件
  • C++标准库文件
  • 其它库的.h文件
  • 本项目内的的.h文件

命名空间namespace

  • 命名空间将全局作用域细分为独立的,具名的作用域
  • 可有效防止全局作用域的命名冲突
  • 不应该使用using指示引入整个命名空间的标识符号
  • 禁止用内联命名空间

接口类

  • 接口是指满足特定条件的类
  • 这些类以Interface为后缀(不强制)

函数使用引用参数

  • 在C语言中,如果函数需要修改变量的值,参数必须为指针
  • 在C++中,函数还可以声明为引用参数
  • 定义引用参数可以防止出现(*pval)++这样丑陋的代码
  • 引用参数对于拷贝构造函数这样的应用也是必需的
  • 同时也更明确地不接受空指针
  • 函数参数列表中,所有引用参数都必须是const

函数返回值后置语法

  • 只有在常规写法(返回类型前置)不便于书写或不便于阅读时使用返回类型后置语法
  • C++11引入了后置返回值的语法
  • 后置返回类型是显式地指定Lambda表达式的返回值的唯一方式

  • 流用来替代printf()和scanf()
  • 有了流,在打印时不需要关心对象的类型
  • 不用担心格式化字符串与参数列表不匹配
  • 流的构造和析构函数会自动打开和关闭对应的文件
  • 不要使用流,除非是日志接口需要
  • 使用printf之类的代替

前置自增和自减

  • 不考虑返回值的话,前置自增(++i)通常要比后置自增(i++)效率更高
  • 因为后置自增(或自减)需要对表达式的值i进行一次拷贝
  • 如果i是迭代器或其他非数值类型,拷贝的代价是比较大的
  • 既然两种自增方式实现的功能一样,为什么不总是使用前置自增呢

constexpr

  • 在C++11里,用constexpr来定义真正的常量
  • 或实现常量初始化
  • 变量可以被声明成constexpr以表示它是真正意义上的常量
  • 即在编译时和运行时都不变
  • 函数或构造函数也可以被声明成constexpr
  • 以用来定义constexpr变量

Lambda表达式

  • 适当使用lambda表达式
  • 别用默认lambda捕获
  • 所有捕获都要显式写出来
  • Lambda表达式是创建匿名函数对象的一种简易途径
  • 常用于把函数当参数传

命名

  • 文件名:http_server_logs.h,http_server_logs.cc
  • 类型性:MyExcitingClass
  • 变量名:string table_name
  • 函数名:AddTableEntry()
  • 命名空间:websearch::index

STL容器与算法

熟悉STL中17种容器及其背后对应的数据结构

  • vector
  • list
  • deque
  • stack
  • queue
  • priority_queue
  • set
  • multiset
  • map
  • multimap
  • unordered_set
  • unordered_multiset
  • unordered_map
  • unordered_multimap
  • array
  • forward_list
  • string

map和unordered_map的区别

  • map背后是红黑树,unordered_map背后是哈希表
  • map是key值有序的,unordered_map是key值无序的
  • 两者内存消耗差不多,但是插入/查找/删除效率unordered_map是map的2到3倍
  • unordered_map是通过链地址法解决冲突的
  • std::map [] operator和insert的区别

priority_queue优先队列的实现

  • priority_queue优先队列,其底层是用堆来实现的
  • 在优先队列中,队首元素一定是当前队列中优先级最高的那一个
  • 在优先队列中,没有front()函数与back()函数
  • 而只能通过top()函数来访问队首元素
  • 也就是优先级最高的元素
  • priority_queue默认为大顶堆

迭代器和迭代器失效iterator

  • 为了提高C++编程的效率,STL中提供了许多容器
  • 有些容器例如vector可以通过脚标索引的方式访问容器里面的数据
  • 但是大部分的容器不能使用这种方式
  • STL中每种容器在实现的时候设计了一个内嵌的iterator类
  • 不同的容器有自己专属的迭代器
  • 使用迭代器来访问容器中的数据
  • 通过迭代器,可以将容器和通用算法结合在一起
  • 只要给予算法不同的迭代器,就可以对不同容器执行相同的操作
  • 迭代器对指针的一些基本操作如*、->、++、==、!=、=进行了重载
  • 使其具有了遍历复杂数据结构的能力
  • 其遍历机制取决于所遍历的数据结构
  • 所有迭代的使用和指针的使用非常相似
  • 通过begin,end函数获取容器的头部和尾部迭代器
  • end迭代器不包含在容器之内
  • 当begin和end返回的迭代器相同时表示容器为空
  • 容器的插入insert和erase操作可能导致迭代器失效
  • 对于erase操作不要使用操作之前的迭代器
  • 因为erase的那个迭代器一定失效了
  • 正确的做法是返回删除操作时候的那个迭代器

容器的线程安全性Thread safety

  • STL为了效率,没有给所有操作加锁
  • 不同线程同时读同一容器对象没关系
  • 不同线程同时写不同的容器对象没关系
  • 但不能同时又读又写同一容器对象的
  • 因此,多线程要同时读写时,还是要自己加锁

STL排序

  • sort,快排加插入排序
  • stable_sort,稳定排序
  • sort_heap,堆排序
  • list.sort,链表归并排序

STL容器的内存管理方式

  • 内存分配器
  • 内存池
  • 内存释放

vector和map的内存释放

  • vector的内存释放
  • map的内存释放
  • 容器删除数据的时候注意迭代器失效
  • vector和map正确的内存释放

编译、链接与调试

排查编译问题常用工具

gcc/g++的区别和使用

  • 后缀为.c的,gcc把它当作是C程序,而g++当作是c++程序
  • 后缀为.cpp的,两者都会认为是c++程序
  • 对于C代码,编译和链接都使用gcc
  • 对于C++代码,编译时可以使用gcc/g++,gcc实际也是调用g++
  • 链接时gcc不能自动和C++使用库链接,因此要使用g++或者gcc -lstdc++

常见gcc编译链接选项

  • -c 只编译并生成目标文件
  • -g 生成调试信息,gdb可以利用该调试信息
  • -o 指定生成的输出文件,可执行程序或者动态链接库文件名
  • -I 编译时添加头文件路径
  • -L 链接时添加库文件路径
  • -D 定义宏,常用于开关控制代码
  • -shared 用于生成共享库.so
  • -Wall 显示所有警告信息,-w不生成任何警告信息
  • -O0选项不进行任何优化,debug会产出和程序预期的结果
  • O1优化会消耗少多的编译时间,它主要对代码的分支,常量以及表达式等进行优化
  • O2会尝试更多的寄存器级的优化以及指令级的优化
  • 它会在编译期间占用更多的内存和编译时间
  • 通常情况下线上代码至少加上O2优化选项
  • -fPIC 位置无关选项,生成动态库时使用
  • 实现真正意义上的多进程共享的.so库
  • -Wl选项告诉编译器将后面的参数传递给链接器
  • -Wl,-Bstatic,指明后面是链接今静态库
  • -Wl,-Bdynamic,指明后面是链接动态库

编译时添加头文件依赖路径

  • -include用来包含头文件
  • 但一般情况下包含头文件都在源码里用#include xxxxxx实现
  • -include参数很少用
  • -I参数是用来指定头文件目录
  • /usr/include目录一般是不用指定的,gcc知道去那里找
  • 但是如果头文件不在/usr/include里我们就要用-I参数指定了
  • 比如头文件放在/myinclude目录里,那编译命令行就要加上-I /myinclude参数了
  • 如果不加你会得到一个”xxxx.h: No such file or directory”的错误
  • -I参数可以用相对路径,比如头文件在当前目录,可以用-I.来指定

排查链接问题常用工具

  • 查看ld链接器的搜索顺序 ld –verbose | grep SEARCH
  • 链接时指定链接目录 -L/dir
  • -Wl,-Bstatic,指明后面是链接今静态库
  • -Wl,-Bdynamic,指明后面是链接动态库
  • 运行时找不到动态库so文件,设置LD_LIBRARY_PATH,添加依赖so文件所在路径
  • 链接完成后使用ldd查看动态库依赖关系
  • 如果依赖的某个库找不到,通过这个命令可以迅速定位问题所在
  • ldd -r,帮助检查是否存在未定义的符号undefine symbol,so库链接状态和错误信息

gdb调试基本使用

对C/C++程序的调试

  • 需要在编译前就加上-g选项
  • $gdb
  • 设置参数:set args 可指定运行时参数
  • (如:set args 10 20 30 40 50)

查看源代码

  • list:简记为l,其作用就是列出程序的源代码,默认每次显示10行
  • list 行号:将显示当前文件以”行号”为中心的前后10行代码
  • list 函数名:将显示”函数名”所在函数的源代码
  • list:不带参数,将接着上一次list命令的,输出下边的内容

设置断点和关闭断点

  • break n(简写b n):在第n行处设置断点
  • break func(简写b func):在函数func()的入口处设置断点
  • info b(info breakpoints):显示当前程序的断点设置情况
  • delete 断点号n:删除第n个断点
  • disable 断点号n:暂停第n个断点
  • clear 行号n:清除第n行的断点

程序调试运行

  • run:简记为r,其作用是运行程序
  • 当遇到断点后,程序会在断点处停止运行
  • 等待用户输入下一步的命令
  • continue(简写c):继续执行,到下一个断点处(或运行结束)
  • next:(简写n),单步跟踪程序
  • 当遇到函数调用时,也不进入此函数体
  • 此命令同step的主要区别是
  • step遇到用户自定义的函数,将步进到函数中去运行
  • 而next则直接调用函数,不会进入到函数体内
  • step(简写s):单步调试如果有函数调用,则进入函数
  • 与命令n不同,n是不进入调用的函数的
  • until:当你厌倦了在一个循环体内单步跟踪时
  • 这个命令可以运行程序直到退出循环体
  • until+行号:运行至某行,不仅仅用来跳出循环
  • finish:运行程序,直到当前函数完成返回
  • 并打印函数返回时的堆栈地址和返回值及参数值等信息
  • call 函数(参数):调用程序中可见的函数,并传递”参数”
  • quit:简记为q,退出gdb

打印程序运行的调试信息

  • print 表达式:简记为p
  • 其中”表达式”可以是任何当前正在被测试程序的有效表达式
  • 比如当前正在调试C语言的程序
  • 那么”表达式”可以是任何C语言的有效表达式
  • 包括数字,变量甚至是函数调用
  • print a:将显示整数a的值
  • print name:将显示字符串name的值
  • print gdb_test(22):将以整数22作为参数调用gdb_test()函数
  • print gdb_test(a):将以变量a作为参数调用gdb_test()函数
  • 扩展info locals:显示当前堆栈页的所有变量

查询运行信息

  • where/bt:当前运行的堆栈列表
  • bt backtrace 显示当前调用堆栈
  • up/down 改变堆栈显示的深度
  • set args 参数:指定运行时的参数
  • show args:查看设置好的参数
  • info program:来查看程序的是否在运行,进程号,被暂停的原因

gdb调试coredump问题

  • Coredump叫做核心转储
  • 它是进程运行时在突然崩溃的那一刻的一个内存快照
  • 操作系统在程序发生异常而异常在进程内部又没有被捕获的情况下
  • 会把进程此刻内存、寄存器状态、运行堆栈等信息转储保存在一个文件里
  • 该文件也是二进制文件,可以使用gdb调试
  • 虽然我们知道进程在coredump的时候会产生core文件
  • 但是有时候却发现进程虽然core了,但是我们却找不到core文件
  • 在ubuntu系统中需要进行设置
  • ulimit -c 可以设置core文件的大小
  • 如果这个值为0.则不会产生core文件
  • 这个值太小,则core文件也不会产生
  • 因为core文件一般都比较大
  • 使用ulimit -c unlimited来设置无限大
  • 则任意情况下都会产生core文件
  • gdb打开core文件时,有显示没有调试信息
  • 因为之前编译的时候没有带上-g选项
  • 没有调试信息是正常的
  • 实际上它也不影响调试core文件
  • 因为调试core文件时,符号信息都来自符号表
  • 用不到调试信息
  • 如下为加上调试信息的效果
  • 调试步骤:
  • $gdb program core_file 进入
  • $ bt或者where # 查看coredump位置
  • 当程序带有调试信息的情况下
  • 我们实际上是可以看到core的地方和代码行的匹配位置
  • 但往往正常发布环境是不会带上调试信息的
  • 因为调试信息通常会占用比较大的存储空间
  • 一般都会在编译的时候把-g选项去掉
  • 这种情况啊也是可以通过core_dump文件找到错误位置的
  • 但这个过程比较复杂

gdb调试线上死锁问题

  • 如果你的程序是一个服务程序
  • 那么你可以指定这个服务程序运行时的进程ID
  • gdb会自动attach上去,并调试
  • 对于服务进程,我们除了使用gdb调试之外
  • 还可以使用pstack跟踪进程栈
  • 这个命令在排查进程问题时非常有用
  • 比如我们发现一个服务一直处于work状态
  • (如假死状态,好似死循环)
  • 使用这个命令就能轻松定位问题所在
  • 可以在一段时间内,多执行几次pstack
  • 若发现代码栈总是停在同一个位置
  • 那个位置就需要重点关注
  • 很可能就是出问题的地方
  • gdb比pstack更加强大
  • gdb可以随意进入进程、线程中改变程序的运行状态和查看程序的运行信息
  • 思考:如何调试死锁?
  • $gdb
  • $pstack pid

undefined symbol问题解决步骤

  • file 检查so或者可执行文件的架构

    1
    2
    $ file _visp.so 
    _visp.so: ELF 64-bit LSB pie executable, x86-64, version 1 (GNU/Linux), dynamically linked, BuildID[sha1]=6503ba6b7545e38e669ab9ed31f86449d8a5f78b, stripped
  • ldd -r _visp.so 命令查看so库链接状态和错误信息

    1
    2
    undefined symbol: __itt_api_version_ptr__3_0	(./_visp.so)
    undefined symbol: __itt_id_create_ptr__3_0 (./_visp.so)
  • c++filt symbol 定位错误在那个C++文件中

    1
    2
    base) terse@ubuntu:~/code/terse-visp$ c++filt __itt_domain_create_ptr__3_0
    __itt_domain_create_ptr__3_0
  • 还可以使用grep -R __itt_domain_create_ptr__3_0 ./
    最终发现这个符号来自XXX/opencv-3.4.6/build/share/OpenCV/3rdparty/libittnotify.a

  • 通过nm命令也能看出该符号确实未定义

    1
    2
    $ nm _visp.so | grep __itt_domain_create_ptr__3_0
    U __itt_domain_create_ptr__3_0

pkg-config找第三方库的头文件和库文件

  • pkg-config能方便使用第三方库和头文件和库文件
  • 其运行原理
  • 它首先根据PKG_CONFIG_PATH环境变量下寻找库对应的pc文件
  • 然后从pc文件中获取该库对应的头文件和库文件的位置信息
  • 例如在项目中需要使用opencv库
  • 该库包含的头文件和库文件比较多
  • 首先查看是否有对应的opencv.pc find /usr -name opencv.pc
  • 查看该路径是否包含在PKG_CONFIG_PATH
  • 使用pkg-config –cflags –libs opencv 查看库对应的头文件和库文件信息
  • pkg-config –modversion opencv 查看版本信息
    参考链接:https://blog.csdn.net/luotuo44/article/details/24836901

cmake中的find_package

  • find_package原理
  • 首先明确一点,cmake本身不提供任何搜索库的便捷方法
  • 所有搜索库并给变量赋值的操作必须由cmake代码完成
  • 比如下面将要提到的FindXXX.cmake和XXXConfig.cmake
  • 只不过,库的作者通常会提供这两个文件
  • 以方便使用者调用
  • find_package采用两种模式搜索库:

Module模式:搜索CMAKE_MODULE_PATH指定路径下的FindXXX.cmake文件,执行该文件从而找到XXX库。其中,具体查找库并给XXX_INCLUDE_DIRS和XXX_LIBRARIES两个变量赋值的操作由FindXXX.cmake模块完成。

Config模式:搜索XXX_DIR指定路径下的XXXConfig.cmake文件,执行该文件从而找到XXX库。其中具体查找库并给XXX_INCLUDE_DIRS和XXX_LIBRARIES两个变量赋值的操作由XXXConfig.cmake模块完成。

两种模式看起来似乎差不多,不过cmake默认采取Module模式,如果Module模式未找到库,才会采取Config模式。如果XXX_DIR路径下找不到XXXConfig.cmake文件,则会找/usr/local/lib/cmake/XXX/中的XXXConfig.cmake文件。总之,Config模式是一个备选策略。通常,库安装时会拷贝一份XXXConfig.cmake到系统目录中,因此在没有显式指定搜索路径时也可以顺利找到。

ldd解决运行时问题

  • 现象:

  • error while loading shared libraries: libopencv_cudabgsegm.so.3.4: cannot open shared object file: No such file or directory

  • ldd ./xxx,发现库文件not found

    libopencv_cudaobjdetect.so.3.4 => not found  
    libopencv_cudalegacy.so.3.4 => not found
    
  • ld.so 动态共享库搜索顺序:

  • ELF可执行文件中动态段DT_RPATH指定;gcc加入链接参数”-Wl,-rpath”指定动态库搜索路径;

  • 环境变量LD_LIBRARY_PATH指定路径;

  • /etc/ld.so.cache中缓存的动态库路径。可以通过修改配置文件/etc/ld.so.conf 增删路径(修改后需要运行ldconfig命令);

  • 默认的 /lib/;

  • 默认的 /usr/lib/

  • 解决办法:

  • 确认系统中是包含这个库文件的

  • pkg-config –libs opencv 查看opencv库的路径

  • export LD_LIBRARY_PATH=/usr/local/lib64,增加运行时加载路径

参考链接:https://www.cnblogs.com/amyzhu/p/8871475.html

makefile和cmake的使用

  • 跟我学些makefile
  • CMake入门实战

性能分析与优化

time

shell time

  • time非常方便获取程序运行的时间
  • 包括用户态时间user、内核态时间sys和实际运行的时间real
  • 我们可以通过(user+sys)/real计算程序CPU占用率
  • 判断程序时CPU密集型还是IO密集型程序

/usr/bin/time

  • Linux中除了shell time,还有/usr/bin/time
  • 它能获取程序运行更多的信息
  • 通常带有-v参数

top

  • top是linux系统的任务管理器
  • 它既能看系统所有任务信息
  • 也能帮助查看单个进程资源使用情况
  • 主要有以下几个功能:
  • 查看系统任务信息
  • 查看CPU使用情况
  • 查看内存使用情况
  • 查看单个进程资源使用情况
  • 除此之外top还提供了一些交互命令

perf

perf stat

  • 做任何事都最好有条有理
  • 老手往往能够做到不慌不忙,循序渐进
  • 而新手则往往东一下,西一下,不知所措
  • 面对一个问题程序,最好采用自顶向下的策略
  • 先整体看看该程序运行时各种统计事件的大概
  • 再针对某些方向深入细节
  • 而不要一下子扎进琐碎细节,会一叶障目的
  • 有些程序慢是因为计算量太大
  • 其多数时间都应该在使用CPU进行计算
  • 这叫做CPU bound型
  • 有些程序慢是因为过多的IO
  • 这种时候其CPU利用率应该不高
  • 这叫做IO bound型
  • 对于CPU bound程序的调优和IO bound的调优是不同的
  • 如果您认同这些说法的话
  • Perf stat应该是您最先使用的一个工具
  • 它通过概括精简的方式提供被调试程序运行的整体情况和汇总数据

perf top

  • Perf top用于实时显示当前系统的性能统计信息
  • 该命令主要用来观察整个系统当前的状态
  • 比如可以通过查看该命令的输出来查看当前系统最耗时的内核函数或某个用户进程

perf record/perf report

  • 使用top和stat之后
  • 这时对程序基本性能有了一个大致的了解
  • 为了优化程序,便需要一些粒度更细的信息
  • 比如说您已经断定目标程序计算量较大
  • 也许是因为有些代码写的不够精简
  • 那么面对长长的代码文件
  • 究竟哪几行代码需要进一步修改呢
  • 这便需要使用perf record记录单个函数级别的统计信息
  • 并使用perf report来显示统计结果
  • 您的调优应该将注意力集中到百分比高的热点代码片段上
  • 假如一段代码只占用整个程序运行时间的0.1%
  • 即使您将其优化到仅剩一条机器指令
  • 恐怕也只能将整体的程序性能提高0.1%
  • 俗话说,好钢用在刀刃上
  • 要优化热点函数

gprof

  • gprof是GNU profiler工具
  • 可以显示程序运行的”flat profile”
  • 包括每个函数的调用次数
  • 每个函数消耗的处理器时间
  • 也可以显示”调用图”
  • 包括函数的调用关系
  • 每个函数调用花费了多少时间
  • 还可以显示”注释的源代码”
  • 是程序源代码的一个复本
  • 标记有程序中每行代码的执行次数

内存问题与valgrind

常见的内存问题

  • 使用未初始化的变量
  • 内存访问越界
  • 内存覆盖
  • 动态内存管理错误
  • 内存泄露

valgrind内存检测

  • valgrind是一个工具集
  • 其中最有名的是Memcheck
  • 它可以帮助我们检查程序中的内存问题
  • 如内存泄漏、越界访问、重复释放等

自定义timer计时器

  • 自己写一个计时器
  • 计算局部函数的时间

常见问题与面试题

如何让类对象只在栈(堆)上分配空间?

  • 只能在栈上建立对象
  • 只能在堆上建立对象

C++不可继承类的实现?

  • 使用final关键字
  • 使用私有构造函数
  • 使用虚析构函数

如何定义和实现一个类的成员函数为回调函数?

  • 友元函数/静态成员函数消除this指针的影响

C++复制构造函数的参数为什么是引用类型?

  • 编译时报错
  • 需要首先调用该类的拷贝构造函数来初始化形参(局部对象)
  • 造成无线循环递归

C++全局对象如何在main函数之前构造和析构?

  • 全局对象的构造和析构顺序
  • 如何控制全局对象的构造和析构顺序

其它常见的问题

  • vector和map的内存释放问题
  • 容器删除数据的时候注意迭代器失效
  • vector和map正确的内存释放
  • C++的iostream的局限
  • STL::list::sort链表归并排序

参考资料

Python程序为什么慢?

  不同的场景下,代码是有不同的要求,大体有三个等级,“管用、更好、更快”。相比C/C++,Python具有较好的开发系效率,但是程序的性能运行速度会差一些。究其原因是Python为了灵活性,牺牲了效率。

  1. 动态类型。对于C/C++等静态类型语言,由于变量的类型固定,变量之间的运算很容易指定特定的函数。而动态类型在运行的时间需要大量if else判断处理,直到找到符合条件的函数。动态类型增加语言的易用性,但是牺牲了程序的运行效率

  2. GIL(Global Interpreter Lock)全局解释锁,CPython在解释执行任何Python代码的时候,首先都需要they acquire GIL when running,release GIL when blocking for I/O。如果没有涉及I/O操作,只有CPU密集型操作时,解释器每隔100一段时间(100ticks)就会释放GIL。GIL是实现Python解释器的(Cython)时所引入的一个概念,不是Python的特性。
    由于GIL的存在,使得Python对于计算密集型任务,多线程threading模块形同虚设,因为线程在实际运行的时候必须获得GIL,而GIL只有一个,因此无法发挥多核的优势。为了绕过GIL的限制,只能使用multiprocessing等多进程模块,每个进程各有一个CPython解释器,也就各有一个GIL。

  3. CPython不支持JIL(Just-In-Time Compiler),JIL 能充分利用程序运行时信息,进行类型推导等优化,对于重复执行的代码段来说加速效果明显。对于CPython如果想使用JIT对计算密集型任务进行优化,可以尝试使用JIT包numba,它能使得相应的函数变成JIT编译。

Python程序优化的思路?

  最近在做一些算法优化方面的工作,简单总结一下思路:

  1. 熟悉算法的整体流程,对于算法代码,最开始尽可能不要使用多线程和多进程方法,
  2. 在1的基础上跑出算法的CPU profile,整体了解算法耗时分布和瓶颈。Python提供的cProfile模块灵活的针对特定函数或者文件产生profile文件,根据profile数据进行代码性能优化。
  1. 程序(算法)本身的剪枝。比如视频追踪中,考虑是否让每个像素点都参与计算?优化后选择梯度变化最大的1w个像素点参与计算,能提高分辨率大的视频追踪效率。
  2. 使用矩阵操作代替循环操作。(get_values())
  3. 任务分解,在理解算法的基础上寻找并行机会,利用多线程或者多进程充分利用机器资源。生产者消费者模型,专门的线程负责图像获取和图形变换,专门的线程负责特征提取和追踪。
  4. 使用C/C++重写效率低的瓶颈部分
  5. 使用GPU计算

title: Python通过swig调用复杂C++库
categories:
- Python

  
  最近项目中需要用到visp库中的模板追踪算法,visp库用C++编写的,代码多,功能丰富。但是,对于项目来说直接调visp库并不方便,因此我们摘取visp库中的所需代码,提供python调用的接口,并根据项目需求进行优化和扩展。开源项目越来越多,以后工作也可能会遇到提取复杂库中部分功能,然后提供python调用的接口,因此这里总结一下,过程并不复杂,但是也遇到一些坑。主要注意以下几点:

  1. 依赖库采用静态方式编译。最开始的时候采用默认的动态编译,导致项目依赖复杂,部署起来非常不方便。要注意的是,visp库依赖opencv库,两个库都要采用静态方式编译。
  2. 提取所需代码,封装成类。提取visp库中的模板追踪算法,封装成类。为了便于后续的优化工作,接口扩展性尽可能好。
  3. **swig实现python调用C++**。有很多方法实现python调用C++,我这里采用swig,适合懒人。

静态编译opencv库

  opencv采用cmake项目管理,通过ccmake可以很方便的设置静态编译选项。BUILD_SHARED_LIBS设置为OFF即为静态编译。另外,为了保持系统整洁,避免安装到系统路径,设置了安装路径,CMAKE_INSTALL_PREFIX=/home/terse/code/terse-visp/opencv-3.4.6/build

  1. git clone https://github.com/opencv/opencv.git
  2. cd opencv-3.4.6
  3. mkdir build && cd build
  4. ccmake ..(关闭动态编译选项,设置安装路径),cmake ..
  5. make -j4
  6. make install

静态编译visp库

  visp库https://github.com/lagadic/visp.git 和opencv库一样都采用cmake管理,编译过程和opencv一样,这里只需要设置静态编译和设置安装路径:
关闭动态编译选项:BUILD_SHARED_LIBS=OFF
设置安装路径: CMAKE_INSTALL_PREFIX=/home/terse/code/terse-visp/visp/build

  1. git clone https://github.com/lagadic/visp.git
  2. cd visp
  3. mkdir build && cd build
  4. ccmake ..(关闭动态编译选项,设置安装路径),cmake ..
  5. make -j4
  6. make install

提取模板追踪算法,封装成C++类

  visp库中提供了模板追踪算法,但是它不能解决遮挡的情况,参考区域很大的时候,追踪速度也很慢,因此在项目中针对这些问题做了一些优化,这个不是本文的重点就不赘述了。下面从visp中摘取的代码,封装成C++类,say_hello成员函数,没有实际用途,只是为了后续的验证python代码的正确性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#ifndef VISP_H_
#define VISP_H_

#include <visp3/io/vpImageIo.h>
#include <visp3/tt/vpTemplateTrackerSSDInverseCompositional.h>
#include <visp3/tt/vpTemplateTrackerWarpHomography.h>
#include <visp3/core/vpException.h>
#include <opencv2/opencv.hpp>
#include <fstream>

class TemplateTracker
{
public:
TemplateTracker();

void SetSampling(unsigned int sample_i, unsigned int sample_j);
void SetLambda(double lamda);
void SetIterationMax(unsigned int n);
void SetPyramidal(unsigned int nlevels, unsigned int level_to_stop);
void SetUseTemplateSelect(bool bselect);
void SetThresholdGradient(float threshold);

int Init(unsigned char* imgData, unsigned int h, unsigned int w, int* ref, unsigned int points_num, bool bshow);
int InitWithMask(unsigned char* imgData, unsigned int h, unsigned int w, int* ref, unsigned int points_num, bool bshow, unsigned char* mask_data,int h2,int w2);

int ComputeH(unsigned char* imgData, unsigned int h,unsigned int w,float* H_matrix,int num);
int ComputeHWithMask(unsigned char* imgData, unsigned int h,unsigned int w,float* H_matrix,int num,unsigned char* mask_data,int h2,int w2);

void Reset();
~TemplateTracker();
void say_hello();


private:
vpTemplateTrackerWarpHomography warp_;
vpTemplateTrackerSSDInverseCompositional tracker_;
int height_, width_;
vpImage<unsigned char> I_;
bool bshow_;
};

#endif /* VISP_H_ */
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
#include "visp.h"  


TemplateTracker::TemplateTracker() :
tracker_(&warp_),
bshow_(false)
{};

void TemplateTracker::SetSampling(unsigned int sample_i, unsigned int sample_j)
{
tracker_.setSampling(sample_i, sample_j);
}

void TemplateTracker::SetLambda(double lamda)
{
tracker_.setLambda(lamda);
}

void TemplateTracker::SetIterationMax(unsigned int n)
{
tracker_.setIterationMax(n);
}

void TemplateTracker::SetPyramidal(unsigned int nlevels, unsigned int level_to_stop)
{
tracker_.setPyramidal(nlevels, level_to_stop);
}

void TemplateTracker::SetUseTemplateSelect(bool bselect)
{
tracker_.setUseTemplateSelect(bselect);
}

void TemplateTracker::SetThresholdGradient(float threshold)
{
tracker_.setThresholdGradient(threshold);
}


int TemplateTracker::Init(unsigned char* imgData, unsigned int h, unsigned int w, int* ref, unsigned int points_num, bool bshow)
{
cv::Mat img_gray(h, w, CV_8UC1, (unsigned char*)(imgData)); //浅拷贝
I_.init(imgData, h, w, true);
height_ = h;
width_ = w;
bshow_ = bshow;
std::vector<vpImagePoint> v_ip;
for (int i = 0; i < points_num/2; i++)
{
vpImagePoint ip(ref[i * 2], ref[i * 2 + 1]);
v_ip.push_back(ip);
}

try{
tracker_.initFromPoints(I_, v_ip);
}catch(vpException &e){
return e.getCode();
}

return 0;
}


int TemplateTracker::InitWithMask(unsigned char* imgData, unsigned int h, unsigned int w, int* ref, unsigned int points_num, bool bshow, unsigned char* mask_data,int h2,int w2)
{
cv::Mat img_gray(h, w, CV_8UC1, (unsigned char*)(imgData)); //浅拷贝
I_.init(imgData, h, w, true);
height_ = h;
width_ = w;
bshow_ = bshow;
if (NULL != mask_data)
{
cv::Mat mask_gray(h, w, CV_8UC1, (unsigned char*)(mask_data)); //浅拷贝
I_.SetMask(mask_gray);
}

std::vector<vpImagePoint> v_ip;
for (int i = 0; i < points_num/2; i++)
{
vpImagePoint ip(ref[i * 2], ref[i * 2 + 1]);
v_ip.push_back(ip);
}

try{
tracker_.initFromPoints(I_, v_ip);
}catch(vpException &e){
return e.getCode();
}

return 0;
}



int TemplateTracker::ComputeH(unsigned char* imgData, unsigned int h,unsigned int w,float* H_matrix,int num)
{
I_.init(imgData, height_, width_, true);
try{
tracker_.track(I_);
}catch(vpTrackingException &e){
std::cout << e.getMessage() << std::endl;
return e.getCode();
}
vpColVector p = tracker_.getp();
vpHomography H = warp_.getHomography(p);
for (int m = 0; m < 3; m++)
{
for (int n = 0; n < 3; n++)
{
H_matrix[m * 3 + n] = H[m][n];
}
}
return 0;
}


int TemplateTracker::ComputeHWithMask(unsigned char* imgData, unsigned int h,unsigned int w,float* H_matrix,int num,unsigned char* mask_data,int h2,int w2)
{
I_.init(imgData, height_, width_, true);
if (NULL != mask_data)
{
cv::Mat mask_gray(height_, width_, CV_8UC1, (unsigned char*)(mask_data)); //浅拷贝
I_.SetMask(mask_gray);
}

try{
tracker_.track(I_);
}catch(vpTrackingException &e){
std::cout << e.getMessage() << std::endl;
return e.getCode();
}
vpColVector p = tracker_.getp();
vpHomography H = warp_.getHomography(p);
for (int m = 0; m < 3; m++)
{
for (int n = 0; n < 3; n++)
{
H_matrix[m * 3 + n] = H[m][n];
}
}
return 0;
}

void TemplateTracker::Reset()
{
tracker_.resetTracker();
}

TemplateTracker::~TemplateTracker(){};

void TemplateTracker::say_hello(){
std::cout << "hello" << std::endl;
}

采用swig实现python调用C++

  python调用C++的方法有很多,例如ctypes、PyObject、Boost.python,采用了swig方法,使用之后感觉确挺方便的。为了给追踪功能提供numpy参数的输入和输出,这里需要引入numpy.i文件。
参考:http://www.swig.org/Doc1.3/Python.html#Python

1. 定义接口文件:visp.i

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* File: visp.i */
%module visp

%{
#define SWIG_FILE_WITH_INIT
#include "visp.h"
%}

%include "numpy.i"

%init %{
import_array();
%}

%apply (unsigned char* IN_ARRAY2, int DIM1, int DIM2) {(unsigned char* imgData, unsigned int h, unsigned int w)}
%apply (unsigned char* IN_ARRAY2, int DIM1, int DIM2) {(unsigned char* mask_data, int h2, int w2)}
%apply (int* IN_ARRAY1, int DIM1) {(int* ref, unsigned int points_num)}
%apply (float* INPLACE_ARRAY1, int DIM1) {(float* H_matrix,int num)}

%include "visp.h"

2. swig 编译visp.i 文件生成C++和py代码,生成visp_wrap.cxx,visp.py

1
swig -c++ -python -py3 visp.i //python3

3. 分别编译visp.cc和visp_wrap.cxx代码

1
2
g++  -O2 -fPIC  -c visp.cc -I/home/terse/code/terse-visp/VispSource/build/include
g++ -O2 -fPIC -c visp_wrap.cxx -I/home/terse/anaconda3/include/python3.6m -I/home/terse/code/terse-visp/VispSource/build/include -I//home/terse/anaconda3/lib/python3.6/site-packages/numpy/core/include/

4. 链接生成_visp.so文件

1
2
3
g++ -shared visp_wrap.o visp.o -L/home/terse/code/terse-visp/VispSource/build/lib -lvisp_ar -lvisp_blob -lvisp_core -lvisp_detection -lvisp_core -lvisp_gui -lvisp_imgproc -lvisp_io -lvisp_klt -lvisp_mbt -lvisp_me -lvisp_robot -lvisp_sensor -lvisp_tt -lvisp_tt_mi -lvisp_vision -lvisp_visual_features -lvisp_vs -lvisp_tt  -lvisp_ar -lvisp_blob -lvisp_core -lvisp_detection -lvisp_core -lvisp_gui -lvisp_imgproc -lvisp_io -lvisp_klt -lvisp_mbt -lvisp_me -lvisp_robot -lvisp_sensor -lvisp_tt -lvisp_tt_mi -lvisp_vision -lvisp_visual_features -lvisp_vs -Wl,-Bstatic -L/home/terse/code/terse-visp/opencv-3.4.6/build/lib -lopencv_dnn -lopencv_ml -lopencv_objdetect -lopencv_shape -lopencv_stitching -lopencv_superres -lopencv_videostab -lopencv_calib3d -lopencv_features2d -lopencv_highgui -lopencv_videoio -lopencv_imgcodecs -lopencv_video -lopencv_photo -lopencv_imgproc -lopencv_flann -lopencv_core -Wl,-Bstatic -L/home/terse/code/terse-visp/opencv-3.4.6/build/share/OpenCV/3rdparty/lib -littnotify -llibprotobuf -llibjasper -lquirc -lippiw -lippicv -Wl,-Bdynamic -lpython3.7m -Wl,-Bdynamic  -llapack  -fopenmp -ldl  -lz -lrt -ltiff -o _visp.so


  这里有个坑纠结了挺久了,最开始生成的_visp.so文件中通过ldd -r 查看一直有几个未定义的符号。排查后发现来自opencv_core库中,通过nm查看,发现libopencv_core.a中是未定义的,而libopencv_sore.so是正常的。最后发现那几个未定义的符号在/home/terse/code/terse-visp/opencv-3.4.6/build/3rdparty/lib/libittnotify.a库中,链接时加入这个库就将未定义的符号解决了。这个链接文件中有许多依赖的库是不需要的,这里就没有仔细排查了,只要没少就能链接成功。

简单测试

  通过ldd -r 检查_visp.so文件没有问题,理论上就没什么问题里,这里通过代码中故意遗留的函数测试一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import visp
import cv2
import numpy as np

def get_frame(cap, frame_index):
pos = cap.get(cv2.CAP_PROP_POS_FRAMES)
if pos != frame_index:
cap.set(cv2.CAP_PROP_POS_FRAMES, frame_index)
ret, frame = cap.read()
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
return gray

if __name__ == '__main__':
video_name = "./test_data/1166/input.mp4"

ref_area = [105, 73, 479, 62, 126, 309, 120, 297, 471, 57, 457, 291]
key_frame = 5520

cap = cv2.VideoCapture(video_name) #video name
tracker = visp.TemplateTracker()
tracker.SetLambda(0.001)
tracker.SetPyramidal(3,0)
tracker.SetIterationMax(200)
tracker.SetSampling(1,1) #x和y方向的降采样率
tracker.say_hello()
img = get_frame(cap, key_frame)
ret_code = tracker.Init(img,ref_area,True)

H_array = np.empty(9,dtype=np.float32)
img = get_frame(cap, key_frame+1)
ret_code = tracker.ComputeH(img,H_array)
print(ret_code,H_array)


title: Python 多线程和多进程
categories:
- Python

  
  最近在做一些算法优化的工作,由于对Python认识不够,开始的入坑使用了多线程。发现在一个四核机器,即使使用多线程,CPU使用率始终在100%左右(一个核)。后来发现Python中并行计算要使用多进程,改成多进程模式后,CPU使用率达到340%,也提升了算法的效率。另外multiprocessing对多线程和多进程做了很好的封装,需要掌握。这里总结下面两个问题:

  1. Python中的并行计算为什么要使用多进程?
  2. Python多线程和多进程简单测试
  3. multiprocessing库的使用

Python中的并行计算为什么要使用多进程?

  Python在并行计算中必须使用多进程的原因是GIL(Global Interpreter Lock,全局解释器锁)。GIL使得在解释执行Python代码时,会产生互斥锁来限制线程对共享资源的访问,直到解释器遇到I/O操作或者操作次数达到一定数目时才会释放GIL。这使得Python一个进程内同一时间只能允许一个线程进行运算”,也就是说多线程无法利用多核CPU。因此:

  1. 对于CPU密集型任务(循环、计算等),由于多线程触发GIL的释放与在竞争,多个线程来回切换损耗资源,因此多线程不但不会提高效率,反而会降低效率。所以计算密集型程序,要使用多进程
  2. 对于I/O密集型代码(文件处理、网络爬虫、sleep等待),开启多线程实际上是**并发(不是并行)**,线程A在IO等待时,会切换到线程B,从而提升效率。
  3. 大多数程序包含CPU和IO操作,但不考虑进程的资源开销,多进程通常都是优于多线程的
  4. 由于Python多线程的问题,因此通常情况下都使用多进程,使用多进程需要注意进程间变量的共享。

Python多线程和多进程简单测试

  • job1是一个完成CPU没有任务IO的死循环,观察CPU使用率,无论使用多少线程数量num,CPU使用率始终在100%左右,也就是说只能利用核的资源。而多进程则可以使用多核资源,num为1时CPU使用率为100%,num为2时CPU使用率接近200%。
  • job2是一个IO密集型的程序,主要的耗时在print系统调用。num=4时,多线程跑了10.81s,cpu使用率93%;多进程只用了3.23s,CPU使用率130%。

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import multiprocessing
import threading

def job1():
'''
full cpu
'''
while True:
continue

NUMS = 100000
def job2():
'''
cpu and io
'''
for i in range(NUMS):
print("hello,world")

def multi_threads(num,job):
threads = []
for i in range(num):
t = threading.Thread(target=job,args=())
threads.append(t)
for t in threads:
t.start()
for t in threads:
t.join()

def multi_process(num,job):
process = []
for i in range(num):
p = multiprocessing.Process(target=job,args=())
process.append(p)
for p in process:
p.start()
for p in process:
p.join()

if __name__ == '__main__':
# multi_threads(4,job1)
# multi_process(4,job1)
# multi_threads(4,job2)
multi_process(4,job2)

multiprocessing的使用

参考:https://docs.python.org/3/library/multiprocessing.html#module-multiprocessing

  1. 单个进程multiprocessing.Process对象,和threading.Thread的API完全一样,start(),join(),参考上文中的测试代码。
  2. 进程池
  3. 进程间对象共享队列multiprocessing.Queue()
  4. 进程同步multiprocessing.Lock()
  5. 进程间状态共享multiprocessing.Value,multiprocessing.Array
  6. multiprocessing.Manager()
  7. 进程池:multiprocessing.Pool()
  • pool.map
  • pool.imap_unordered
  • pool.apply_async

  Python多线程和多进程的使用非常方面,因为multiprocessing提供了非常好的封装。为了方便设置线程和进程的数量,通常都会使用池pool技术。

1
2
from multiprocessing.dummy import Pool as DummyPool   # thread pool
from multiprocessing import Pool # process pool

multilprocessing包的使用可参考:

1、linux基础命令

  • 帮助命令:man、info
  • 查找命令路径:which、whereis
  • 查看文件文件个数:find ./ | wc -l
  • 以时间顺序显示目录项:ls -lrt
  • 查看文件时同时显示行数:cat -n xxx
  • 查看两个文件的差别:diff file1 file2
  • 动态显示文本最新信息,常用于查看日志: tail -f xxx.log
  • 软连接/硬链接: ln cc ccAgain 和 ln -s cc ccAgain
  • command1 && command2
  • comamand1 || command2
  • 查找txt和pdf文件:find . ( -name “.txt” -o -name “.pdf” ) -print
  • find查找文件时指定深度:find . -maxdepth 1 -type f
  • find只查找目录:find . -type d -print
  • 文本处理
  • 打包:taf -cvf xxx.tar . 解包: tar -xvf xxx.tar
  • 压缩与解压:-z 解压gz文件;-j解压bz2;-J解压xz文件
  • grep 查找文件中指定字符出现的次数
1
cat Temp\ Query\ 1_20230914-171937.csv | grep  "\"sop_v3_user" | grep -v "xxxx" | awk -F ',' '{print $2,$5,$6}' | sort | uniq -c | sort -rk 2

2、系统信息查看工具

  • 查看操作系统发行版:lsb_release -a
  • 查看内核版本信息:uname -a
  • 查看cpu信息:cat /proc/cpuinfo
  • 查看cpu核数:cat /proc/cpuinfo | grep processor | wc -l
  • 查看内存信息:cat /proc/meminfo
  • 显示架构:arch
  • 查看进程间ipc资源情况:ipcs
  • 显示当前所有的系统资源limit信息: ulimit -a
  • 对生成的core文件的大小不进行限制:ulimit -c unlimited

3、系统资源管理和监控

  • 查询正在运行的进程信息:ps -ef 或者 ps -ajx
  • 查询某用户的进程: ps -ef | grep username 或者 ps -lu username
  • 实时显示进程信息: top linux下的任务管理器,内存VIRT和RES
  • 查看用户打开的文件: lsof -u username
  • 查看某进程打开的文件: lsof -p pid
  • 杀死某进程:kill -9 pid
  • pmap输出进程内存你的状况,用来分析线程堆栈
  • 查看内存使用量:free -m 或者 vmstat n m
  • 查看磁盘使用情况:df -h
  • du -ha –max-depth=1
  • iostat 监视I/O子系统,ubuntu安装systat。通过iostat方便查看CPU、网卡、tty设备、磁盘、CD-ROM 等等设备的活动情况, 负载信息
  • sar 找出系统瓶颈的利器
    *ubuntu系统下,默认可能没有安装这个包,使用apt-get install sysstat 来安装;
    安装完毕,将性能收集工具的开关打开: vi /etc/default/sysstat
    设置 ENABLED=”true”
    启动这个工具来收集系统性能数据: /etc/init.d/sysstat start.

4、网络工具

  • 查看网络流量信息iftop
  • netstat命令用于显示各种网络相关信息
  • 查询某端口port被某个进程占用:netstat -antp | grep port,然后使用ps pid查询进程名称
  • 也可以使用lsof -i:port 直接查询该端口的进程
  • ping 测试网络连通情况
  • traceroute IP 探测前往ip的路由信息
  • 直接下载文件或者网页:wget
  • 网络远程复制:scp -r localpath ID@host:path
  • 使用ssh协议下载: scp -r ID@host:path localpath
  • nc服务器编程常用,既可以作为客户端又可以指定端口作为服务端。
  • 查看网络端口使用情况:https://www.runoob.com/w3cnote/linux-check-port-usage.html

5、环境变量

  • 全局/etc/profile->/etc/profile.d;
  • 读取当前用户下面的:/.bash_profile->/.bash_login->~/.profile
  • 读取当前用户目录下面的:~/.bashrc
  • export环境变量,退出失效

6、查看GPU信息

  • 查看gpu信息 nvidia-smi
  • 查看gpu驱动版本信息 cat /proc/driver/nvidia/version
  • pkgconfig? PKG_CONFIG_PATH环境变量

7、测试系统磁盘的性能

dd是Linux/UNIX 下的一个非常有用的命令,作用是用指定大小的块拷贝一个文件,并在拷贝的同时进行指定的转换。另外在linux中,有两个特殊的设备:/dev/null:回收站、无底洞,经常作为写端,不会产生IO,/dev/zero产生字符,经常作为读端,也不会产生IO。
(1)测试磁盘写能力
dd if=/dev/zero of=/test1.img bs=4k count=10000
因为/dev//zero是一个伪设备,它只产生空字符流,对它不会产生IO,所以,IO都会集中在of文件中,of文件只用于写,所以这个命令相当于测试磁盘的写能力。命令结尾添加oflag=direct将跳过内存缓存,添加oflag=sync将跳过hdd缓存。
(2)测试磁盘读能力
dd if=/dev/sda of=/dev/null bs=4k count=10000
因为/dev/sdb是一个物理分区,对它的读取会产生IO,/dev/null是伪设备,相当于黑洞,of到该设备不会产生IO,所以,这个命令的IO只发生在/dev/sdb上,也相当于测试磁盘的读能力。
(3)测试同时读写能力
time dd if=/dev/sda of=/test1.img bs=4k count=10000
在这个命令下,一个是物理分区,一个是实际的文件,对它们的读写都会产生IO(对/dev/sda是读,对/test.img是写),假设它们都在一个磁盘中,这个命令就相当于测试磁盘的同时读写能力。

8、使用dd和nc命令测试网络性能

nc是netcat的简写,有着网络界的瑞士军刀美誉。因为它短小精悍、功能实用,被设计为一个简单、可靠的网络工具
(1)实现任意TCP/UDP端口的侦听,nc可以作为server以TCP或UDP方式侦听指定端口
(2)端口的扫描,nc可以作为client发起TCP或UDP连接
(3)机器之间传输文件
(4)机器之间网络测速
nc命令有个-l参数可以用来监听指定端口,因此我们要完成上面的功能,就只需要简单的从/dev/zero或者其他虚拟设备读入数据:

time nc -l -p 5001 < /test.img

然后另外一台电脑使用nc来连接到这个端口并读入数据:
time nc 192.168.0.11 5001 > /dev/null
上面的测试的结果中,是从磁盘读数据通过网络获取,通过time命令或缺时间参数,可以计算出网络的性能。更准备的测试应该从/dev/zero中多数据会更好一些

参考:https://linuxtools-rst.readthedocs.io/zh_CN/latest/tool/index.html

TCP和UDP协议

  1. tcp头格式,其20个字节包含哪些内容? udp头部格式,其8个字节分别包含哪些内容?

  2. 为什么 UDP 头部没有「首部长度」字段,而 TCP 头部有「首部长度」字段呢?原因是 TCP 有可变长的「选项」字段,而 UDP 头部长度则是不会变化的,无需多一个字段去记录 UDP 的首部长度

  3. tcp和udp的区别以及应用场景

    • TCP是面向连接的,而UDP是不需要建立连接的
    • TCP 是一对一的两点服务,UDP 支持一对一、一对多、多对多的交互通信
    • 可靠性,TCP 是可靠交付数据的,数据可以无差错、不丢失、不重复、按需到达。UDP 是尽最大努力交付,不保证可靠交付数据。
    • TCP有拥塞控制、流量控制
    • 首部开销,TCP 首部长度较长,会有一定的开销,首部在没有使用「选项」字段时是 20 个字节,如果使用了「选项」字段则会变长的。UDP 首部只有 8 个字节,并且是固定不变的,开销较小。
    • 传输方式,TCP 是流式传输,没有边界,但保证顺序和可靠。UDP 是一个包一个包的发送,是有边界的,但可能会丢包和乱序

    TCP 和 UDP 应用场景:由于 TCP 是面向连接,能保证数据的可靠性交付,因此经常用于,FTP 文件传输HTTP / HTTPS,由于 UDP 面向无连接,它可以随时发送数据,再加上UDP本身的处理既简单又高效,因此经常用于:包总量较少的通信,如 DNS 、SNMP 等视频、音频等多媒体通信广播通信

  4. TCP协议如何保证可靠传输?

    • 三次握手四次挥手确保连接的建立和释放
    • 超时重发:数据切块发送,等待确认,超时未确认会重发
    • 数据完整性校验:TCP首部中数据有端到端的校验和,接收方会校验,一旦出错将丢弃且不确认收到此报文
    • 根据序列码进行数据的排序和去重
    • 根据接收端缓冲区大小做流量控制
    • 根据网络环境做拥塞控制。当网络拥塞时,会减少数据的发送
  5. TCP怎么通过三次握手和四次挥手建立可靠连接以及需要注意的问题

    • 分别准确画出三次握手和四次挥手状态转换图 从上面的过程可以发现第三次握手是可以携带数据的,前两次握手是不可以携带数据的,这也是面试常问的题
    • 为什么需要三次握手? 通过三次握手实现了同步序列号和避免了旧的重复连接初始化造成混乱,浪费服务器资源,两个作用
    • 为什么需要四次挥手?全双工通信
    • time_wait状态什么作用? 防止之前的报文造成新连接数据混乱,通过2msl使前一连接数据失效;确保ack报文发送给服务端。
  6. 超时重传和快速重传

    • 客户端通过定时器在指定时间内未发现会收到ack信息就认为进行超时重传
    • 客户端收到连续三个重复ack信息就会发起快速重传而不用等待超时重传
  7. 如何解决可能出现的乱序和重复数据问题

  8. TCP流量控制和滑动窗口

    • 为了提高数据传输的小路,tcp避免了一问一答式的消息传输策略
    • 通过累积确认ACK的方式提高效率
    • 在累积确认时通过接收窗口进行流量控制

  9. tcp拥塞控制和拥塞窗口?
    TCP拥塞控制

    • tcp在数据发送时会结合整个网络环境调整数据发送的速率
    • 发送者如何判断拥塞已经发生的?发送超时,或者说TCP重传定时器溢出;接收到重复的确认报文段
    • 快重传算法(接收端到失序的报文段立即重传、发送端一旦接收三个重复的确认报文段,立即重传,不用等定时器)
  10. TCP 的连接状态查看,在 Linux 可以通过 netstat -napt 命令查看

  11. 什么是SYN攻击,怎么避免SYN攻击?

  • SYN攻击属于DOS攻击的一种,它利用TCP协议缺陷,通过发送大量的半连接请求,耗费CPU和内存资源。SYN攻击除了能影响主机外,还可以危害路由器、防火墙等网络系统,事实上SYN攻击并不管目标是什么系统,只要这些系统打开TCP服务就可以实施。从上图可看到,服务器接收到连接请求(syn=j),将此信息加入未连接队列,并发送请求包给客户(syn=k,ack=j+1),此时进入SYN_RECV状态。当服务器未收到客户端的确认包时,重发请求包,一直到超时,才将此条目从未连接队列删除。配合IP欺骗,SYN攻击能达到很好的效果,通常,客户端在短时间内伪造大量不存在的IP地址,向服务器不断地发送syn包,服务器回复确认包,并等待客户的确认,由于源地址是不存在的,服务器需要不断的重发直至超时,这些伪造的SYN包将长时间占用未连接队列,正常的SYN请求被丢弃,目标系统运行缓慢,严重者引起网络堵塞甚至系统瘫痪。
  1. 如何解决close_wait和time_wait过多的问题?

    • CLOSE_WAIT,只会发生在客户端先关闭连接的时候,但已经收到客户端的fin包,但服务器还没有关闭的时候会产生这个状态,如果服务器产生大量的这种连接一般是程序问题导致的,如部分情况下不会执行socket的close方法,解决方法是查程序
    • TIME_WAIT,time_wait是一个需要特别注意的状态,他本身是一个正常的状态,只在主动断开那方出现,每次tcp主动断开都会有这个状态的,维持这个状态的时间是2个msl周期(2分钟),设计这个状态的目的是为了防止我发了ack包对方没有收到可以重发。那如何解决出现大量的time_wait连接呢?千万不要把tcp_tw_recycle改成1,这个我再后面介绍,正确的姿势应该是降低msl周期,也就是tcp_fin_timeout值,同时增加time_wait的队列(tcp_max_tw_buckets),防止满了。
  2. 什么是TCP粘包,应用层怎么解决,http是怎么解决的。tcp是字节流,需要根据特殊字符和长度信息将消息分开

  3. udp协议怎么做可靠传输?
    由于在传输层UDP已经是不可靠的连接,那就要在应用层自己实现一些保障可靠传输的机制,简单来讲,要使用UDP来构建可靠的面向连接的数据传输,就要实现类似于TCP协议的,超时重传(定时器),有序接受 (添加包序号),应答确认 (Seq/Ack应答机制),滑动窗口流量控制等机制 (滑动窗口协议),等于说要在传输层的上一层(或者直接在应用层)实现TCP协议的可靠数据传输机制,比如使用UDP数据包+序列号,UDP数据包+时间戳等方法。目前已经有一些实现UDP可靠传输的机制,比如UDT(UDP-based Data Transfer Protocol)基于UDP的数据传输协议(UDP-based Data Transfer Protocol,简称UDT)是一种互联网数据传输协议。UDT的主要目的是支持高速广域网上的海量数据传输,而互联网上的标准数据传输协议TCP在高带宽长距离网络上性能很差。 顾名思义,UDT建于UDP之上,并引入新的拥塞控制和数据可靠性控制机制。UDT是面向连接的双向的应用层协议。它同时支持可靠的数据流传输和部分可靠的数据报传输。 由于UDT完全在UDP上实现,它也可以应用在除了高速数据传输之外的其它应用领域,例如点到点技术(P2P),防火墙穿透,多媒体数据传输等等

  4. TCP 保活机制KeepAlive?其局限性?Http的keep-alive?为什么应用层也经常做心跳检查?

    • TCP KeepAlive 的基本原理是,隔一段时间给连接对端发送一个探测包,如果收到对方回应的 ACK,则认为连接还是存活的,在超过一定重试次数之后还是没有收到对方的回应,则丢弃该 TCP 连接。TCP-Keepalive-HOWTO 有对 TCP KeepAlive 特性的详细介绍,有兴趣的同学可以参考。
    • TCP KeepAlive 的局限。首先 TCP KeepAlive 监测的方式是发送一个 probe 包,会给网络带来额外的流量,另外 TCP KeepAlive 只能在内核层级监测连接的存活与否,而连接的存活不一定代表服务的可用。例如当一个服务器 CPU 进程服务器占用达到 100%,已经卡死不能响应请求了,此时 TCP KeepAlive 依然会认为连接是存活的。因此 TCP KeepAlive 对于应用层程序的价值是相对较小的。需要做连接保活的应用层程序,例如 QQ,往往会在应用层实现自己的心跳功能。
      除了TCP自带的Keeplive机制,实现业务中经常在业务层面定制“心跳”功能,主要有以下几点考虑:
    • TCP自带的keepalive使用简单,仅提供连接是否存活的功能
    • 应用层心跳包不依赖于传输协议,支持tcp和udp
    • 应用层心跳包可以定制,可以应对更加复杂的情况或者传输一些额外的消息
    • Keepalive仅仅代表连接保持着,而心跳往往还表示服务正常工作
      在 HTTP 1.0 时期,每个 TCP 连接只会被一个 HTTP Transaction(请求加响应)使用,请求时建立,请求完成释放连接。当网页内容越来越复杂,包含大量图片、CSS 等资源之后,这种模式效率就显得太低了。所以,在 HTTP 1.1 中,引入了 HTTP persistent connection 的概念,也称为 HTTP keep-alive,目的是复用TCP连接,在一个TCP连接上进行多次的HTTP请求从而提高性能。HTTP1.0中默认是关闭的,需要在HTTP头加入”Connection: Keep-Alive”,才能启用Keep-Alive;HTTP1.1中默认启用Keep-Alive,加入”Connection: close “,才关闭。两者在写法上不同,http keep-alive 中间有个”-“符号。 HTTP协议的keep-alive 意图在于连接复用,同一个连接上串行方式传递请求-响应数据。TCP的keepalive机制意图在于保活、心跳,检测连接错误。
  5. TCP 协议性能问题分析?

    • TCP 的拥塞控制在发生丢包时会进行退让,减少能够发送的数据段数量,但是丢包并不一定意味着网络拥塞,更多的可能是网络状况较差;
    • TCP 的三次握手带来了额外开销,这些开销不只包括需要传输更多的数据,还增加了首次传输数据的网络延迟;
    • TCP 的重传机制在数据包丢失时可能会重新传输已经成功接收的数据段,造成带宽的浪费;
  6. QUIC 是如何解决TCP 性能瓶颈的?

  7. 科普:QUIC协议原理分析

http和https

  1. HTTP协议协议格式详解

    • 请求行(request line)。请求方法、域名、协议版本。
    • 请求头部(header)从第二行起为请求头部,Host指出请求的目的地(主机域名);User-Agent是客户端的信息,它是检测浏览器类型的重要信息,由浏览器定义,并且在每个请求中自动发送
    • 空行
    • 请求数据
  2. http 常见的状态码有哪些?

    • 200 成功
    • 3xx重定向相关,301 永久重定向,302临时重定向
    • 4xx客户端错误,400请求报文有问题,403服务器禁止访问资源,404资源不存在
    • 5xx服务器内部错误,501 请求的功能暂不支持,502 服务器逻辑有问题,503 服务器繁忙
  3. get 和 post 区别

    • GET参数通过URL传递,POST放在Request body中
    • GET请求只能进行url编码,而POST支持多种编码方式
    • GET请求在URL中传送的参数是有长度限制的,而POST没有
    • GET比POST更不安全,因为参数直接暴露在URL上,所以不能用来传递敏感信息。
    • GET请求参数会被完整保留在浏览器历史记录里,而POST中的参数不会被保留。
  4. https的工作原理和流程

  5. http和https的区别

    • http采用明文传输,http+ssl的加密传输
    • http是80端口,https是443端口
    • HTTP的连接很简单,是无状态的;HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比HTTP协议安全
  6. 浏览器输入http://www.baidu.com
    事件顺序
    (1) 浏览器获取输入的域名www.baidu.com
    (2) 浏览器向DNS请求解析www.baidu.com的IP地址
    (3) 域名系统DNS解析出百度服务器的IP地址
    (4) 浏览器与该服务器建立TCP连接(默认端口号80)
    (5) 浏览器发出HTTP请求,请求百度首页
    (6) 服务器通过HTTP响应把首页文件发送给浏览器
    (7) TCP连接释放
    (8) 浏览器将首页文件进行解析,并将Web页显示给用户。

  7. http长连接和短连接?http长连接和短连接以及keep-Alive的含义,HTTP 长连接不可能一直保持,例如 Keep-Alive: timeout=5, max=100,表示这个TCP通道可以保持5秒,max=100,表示这个长连接最多接收100次请求就断开。

  8. http cookie和session

    • Cookie和Session都是客户端与服务器之间保持状态的解决方案,具体来说,cookie机制采用的是在客户端保持状态的方案,而session机制采用的是在服务器端保持状态的方案
    • Cookie实际上是一小段的文本信息。客户端请求服务器,如果服务器需要记录该用户状态,就使用response向客户端浏览器颁发一个Cookie,而客户端浏览器会把Cookie保存起来。当浏览器再请求该网站时,浏览器把请求的网址连同该Cookie一同提交给服务器,服务器检查该Cookie,以此来辨认用户状态。服务器还可以根据需要修改Cookie的内容
  9. http1.0,tttp1.1,http2.0,http 3.0各有什么变化

    • http 1.0
    • http 1.1, 长连接
    • http 2.0,二进制压缩+连接复用
    • http QUIC,udp+ssl
  10. HTTP/3 竟然基于 UDP,HTTP 协议这些年都经历了啥?

  11. 使用curl

  12. https中间人攻击原理以及防御措施

  13. 如何理解http的无连接和无状态的特点?

  14. 半链接和Sync 攻击原理及防范技术


资料来源:OSI 7层模型

超文本传输协议(HTTPS/HTTP1.1/HTTP2/HTTP3)

https://aws.amazon.com/cn/compare/the-difference-between-https-and-http/

HTTP 是一种在客户端和服务器之间编码和传输数据的方法。它是一个请求/响应协议:客户端和服务端针对相关内容和完成状态信息的请求和响应。HTTP 是独立的,允许请求和响应流经许多执行负载均衡,缓存,加密和压缩的中间路由器和服务器。

一个基本的 HTTP 请求由一个动词(方法)和一个资源(端点)组成。 以下是常见的 HTTP 动词:

动词 描述 *幂等 安全性 可缓存
GET 读取资源 Yes Yes Yes
POST 创建资源或触发处理数据的进程 No No Yes,如果回应包含刷新信息
PUT 创建或替换资源 Yes No No
DELETE 删除资源 Yes No No

  • HTTPS 是基于 HTTP 的安全版本,通过使用 SSL 或 TLS 加密和身份验证通信。
  • HTTP/1.1 是 HTTP 的第一个主要版本,引入了持久连接、管道化请求等特性。
  • HTTP/2 是 HTTP 的第二个主要版本,使用二进制协议,引入了多路复用、头部压缩、服务器推送等特性。
  • HTTP/3 是 HTTP 的第三个主要版本,基于 QUIC 协议,使用 UDP,提供更快的传输速度和更好的性能

多次执行不会产生不同的结果

HTTP 是依赖于较低级协议(如 TCPUDP)的应用层协议。

来源及延伸阅读:HTTP

传输控制协议(TCP)


资料来源:如何制作多人游戏

TCP 是通过 IP 网络的面向连接的协议。 使用握手建立和断开连接。 发送的所有数据包保证以原始顺序到达目的地,用以下措施保证数据包不被损坏:

如果发送者没有收到正确的响应,它将重新发送数据包。如果多次超时,连接就会断开。TCP 实行流量控制拥塞控制。这些确保措施会导致延迟,而且通常导致传输效率比 UDP 低。

为了确保高吞吐量,Web 服务器可以保持大量的 TCP 连接,从而导致高内存使用。在 Web 服务器线程间拥有大量开放连接可能开销巨大,消耗资源过多,也就是说,一个 memcached 服务器。连接池 可以帮助除了在适用的情况下切换到 UDP。

TCP 对于需要高可靠性但时间紧迫的应用程序很有用。比如包括 Web 服务器,数据库信息,SMTP,FTP 和 SSH。

以下情况使用 TCP 代替 UDP:

  • 你需要数据完好无损。
  • 你想对网络吞吐量自动进行最佳评估。

用户数据报协议(UDP)


资料来源:如何制作多人游戏

UDP 是无连接的。数据报(类似于数据包)只在数据报级别有保证。数据报可能会无序的到达目的地,也有可能会遗失。UDP 不支持拥塞控制。虽然不如 TCP 那样有保证,但 UDP 通常效率更高。

UDP 可以通过广播将数据报发送至子网内的所有设备。这对 DHCP 很有用,因为子网内的设备还没有分配 IP 地址,而 IP 对于 TCP 是必须的。

UDP 可靠性更低但适合用在网络电话、视频聊天,流媒体和实时多人游戏上。

以下情况使用 UDP 代替 TCP:

  • 你需要低延迟
  • 相对于数据丢失更糟的是数据延迟
  • 你想实现自己的错误校正方法

来源及延伸阅读:TCP 与 UDP

远程过程调用协议(RPC)


Source: Crack the system design interview

在 RPC 中,客户端会去调用另一个地址空间(通常是一个远程服务器)里的方法。调用代码看起来就像是调用的是一个本地方法,客户端和服务器交互的具体过程被抽象。远程调用相对于本地调用一般较慢而且可靠性更差,因此区分两者是有帮助的。热门的 RPC 框架包括 ProtobufThriftAvro

RPC 是一个“请求-响应”协议:

  • 客户端程序 ── 调用客户端存根程序。就像调用本地方法一样,参数会被压入栈中。
  • 客户端 stub 程序 ── 将请求过程的 id 和参数打包进请求信息中。
  • 客户端通信模块 ── 将信息从客户端发送至服务端。
  • 服务端通信模块 ── 将接受的包传给服务端存根程序。
  • 服务端 stub 程序 ── 将结果解包,依据过程 id 调用服务端方法并将参数传递过去。

RPC 调用示例:

1
2
3
4
5
6
7
GET /someoperation?data=anId

POST /anotheroperation
{
"data":"anId";
"anotherdata": "another value"
}

RPC 专注于暴露方法。RPC 通常用于处理内部通讯的性能问题,这样你可以手动处理本地调用以更好的适应你的情况。

当以下情况时选择本地库(也就是 SDK):

  • 你知道你的目标平台。
  • 你想控制如何访问你的“逻辑”。
  • 你想对发生在你的库中的错误进行控制。
  • 性能和终端用户体验是你最关心的事。

遵循 REST 的 HTTP API 往往更适用于公共 API。

缺点:RPC

  • RPC 客户端与服务实现捆绑地很紧密。
  • 一个新的 API 必须在每一个操作或者用例中定义。
  • RPC 很难调试。
  • 你可能没办法很方便的去修改现有的技术。举个例子,如果你希望在 Squid 这样的缓存服务器上确保 RPC 被正确缓存的话可能需要一些额外的努力了。

表述性状态转移(REST)

REST 是一种强制的客户端/服务端架构设计模型,客户端基于服务端管理的一系列资源操作。服务端提供修改或获取资源的接口。所有的通信必须是无状态和可缓存的。

RESTful 接口有四条规则:

  • 标志资源(HTTP 里的 URI) ── 无论什么操作都使用同一个 URI。
  • 表示的改变(HTTP 的动作) ── 使用动作, headers 和 body。
  • 可自我描述的错误信息(HTTP 中的 status code) ── 使用状态码,不要重新造轮子。
  • HATEOAS(HTTP 中的HTML 接口) ── 你的 web 服务器应该能够通过浏览器访问。

REST 请求的例子:

1
2
3
4
GET /someresources/anId

PUT /someresources/anId
{"anotherdata": "another value"}

REST 关注于暴露数据。它减少了客户端/服务端的耦合程度,经常用于公共 HTTP API 接口设计。REST 使用更通常与规范化的方法来通过 URI 暴露资源,通过 header 来表述并通过 GET、POST、PUT、DELETE 和 PATCH 这些动作来进行操作。因为无状态的特性,REST 易于横向扩展和隔离。

缺点:REST

  • 由于 REST 将重点放在暴露数据,所以当资源不是自然组织的或者结构复杂的时候它可能无法很好的适应。举个例子,返回过去一小时中与特定事件集匹配的更新记录这种操作就很难表示为路径。使用 REST,可能会使用 URI 路径,查询参数和可能的请求体来实现。
  • REST 一般依赖几个动作(GET、POST、PUT、DELETE 和 PATCH),但有时候仅仅这些没法满足你的需要。举个例子,将过期的文档移动到归档文件夹里去,这样的操作可能没法简单的用上面这几个 verbs 表达。
  • 为了渲染单个页面,获取被嵌套在层级结构中的复杂资源需要客户端,服务器之间多次往返通信。例如,获取博客内容及其关联评论。对于使用不确定网络环境的移动应用来说,这些多次往返通信是非常麻烦的。
  • 随着时间的推移,更多的字段可能会被添加到 API 响应中,较旧的客户端将会接收到所有新的数据字段,即使是那些它们不需要的字段,结果它会增加负载大小并引起更大的延迟。

RPC 与 REST 比较

操作 RPC REST
注册 POST /signup POST /persons
注销 POST /resign
{
“personid”: “1234”
}
DELETE /persons/1234
读取用户信息 GET /readPerson?personid=1234 GET /persons/1234
读取用户物品列表 GET /readUsersItemsList?personid=1234 GET /persons/1234/items
向用户物品列表添加一项 POST /addItemToUsersItemsList
{
“personid”: “1234”;
“itemid”: “456”
}
POST /persons/1234/items
{
“itemid”: “456”
}
更新一个物品 POST /modifyItem
{
“itemid”: “456”;
“key”: “value”
}
PUT /items/456
{
“key”: “value”
}
删除一个物品 POST /removeItem
{
“itemid”: “456”
}
DELETE /items/456

资料来源:你真的知道你为什么更喜欢 REST 而不是 RPC 吗

网络通讯协议

OSI 七层网络模型


资料来源:OSI 7层模型

常用的应用层协议

HTTP (Hypertext Transfer Protocol)

用途:主要用于Web浏览器和服务器之间的通信,是万维网的数据传输基础。
特点:无状态、请求-响应模式。
版本:HTTP/1.1, HTTP/2, HTTP/3

FTP (File Transfer Protocol)

用途:用于在客户端和服务器之间传输文件。
特点:支持文件上传和下载,支持匿名访问和身份验证。

邮件协议

  • SMTP (Simple Mail Transfer Protocol)
    用途:用于发送电子邮件。
    特点:主要用于邮件服务器之间的邮件传输。
  • POP3 (Post Office Protocol 3)
    用途:用于从邮件服务器下载邮件到本地客户端。
    特点:下载后邮件通常会从服务器删除。
  • IMAP (Internet Message Access Protocol)
    用途:用于从邮件服务器读取邮件。
    特点:支持在服务器上管理和存储邮件,客户端和服务器邮件同步

WebSocket

用途:提供全双工通信的协议,允许在客户端和服务器之间建立持久连接。
特点:低延迟、实时通信、减少HTTP请求开销。
为什么需要websocket

WebRTC (Web Real-Time Communication)

用途:用于实现浏览器和移动应用之间的实时音视频通信和数据共享。
特点:P2P通信、低延迟、高质量音视频传输。
webRTC

MQTT (Message Queuing Telemetry Transport)

用途:轻量级的发布/订阅消息传输协议,常用于物联网(IoT)设备之间的通信。
特点:低带宽、低能耗、可靠性高

超文本传输协议

  • aws http 选择介绍
  • HTTPS 是基于 HTTP 的安全版本,通过使用 SSL 或 TLS 加密和身份验证通信。
  • HTTP/1.1 是 HTTP 的第一个主要版本,引入了持久连接、管道化请求等特性。
  • HTTP/2 是 HTTP 的第二个主要版本,使用二进制协议,引入了多路复用、头部压缩、服务器推送等特性。
  • HTTP/3 是 HTTP 的第三个主要版本,基于 QUIC 协议,使用 UDP,提供更快的传输速度和更好的性能

其它

  1. https://blog.csdn.net/justloveyou_/article/details/78303617
  2. 图解https的过程:https://segmentfault.com/a/1190000021494676
  3. 35 张图解:被问千百遍的 TCP 三次握手和四次挥手面试题
  4. 30张图解: TCP 重传、滑动窗口、流量控制、拥塞控制
  5. 硬核!30 张图解 HTTP 常见的面试题

一、

今年是2018年,腾讯20周年。我30周岁,刚好在腾讯工作满8年。

我从来没有想过自己会在同一家公司工作8年。因为4年足以读完大学,6年能让小孩读完小学,8年漫长得不可思议。

2010年,我刚大学毕业,加入腾讯。那一天,学生思维的我,不免以学生的尺度定计划:三年的时间,我应该足够从这一所“社会大学”毕业吧。

因此,我追赶时间,以这个截止日为目标,第一年学习高效地完成工作,第二年学习带新人,第三年学习影响力,翻译了一本前端书,和一本设计书。

我一步步从助理UI工程师晋级到高级UI工程师,先是积极响应需求,后来主动找事情做。我低着头,做事情非常“用力”,自信能把交给我的事情都做得很好。

我的博客文章80%都是头三年写的,现在回头看有很多幼稚的想法,但持续想和写才能提高。反过来说,要是现在还觉得好,那才糟糕。

二、

2013年,三年之痒。我开始觉得日常工作毫无挑战,考评时连续“优秀”跟“超出预期”拿到手软,但与此同时,也迎来新的工作挑战。

那时,我的领导问我以后的发展意向,是想继续研究技术深度,还是管理团队。我说如果有机会,尽量管理团队吧。

因为以我的理解,并不存在两种选项。这个问题就像“给你加工资,好不好啊”一样没有意义。学而优则仕,骨干不去领导团队,可能有点不负责任(现在想来很自恋,呵呵)。

虽然给了领导这样的答复,也开始进行正式的管理培训,但我内心还恋恋不舍想保留一点匠人心态。

个人能力要继续提高,我就开辟新的赛道:影响力。

2014年我认真地投入到写作练习中,在“26岁总结”中写下这段文字:

“在很多场景下我们都需要写作,我们要写短小的RTX,长一点的邮件,以及更长一点的分享文章、博客和专栏。关于写作,我觉得最有趣的一个事实是,优秀的写作者跟平庸的写作者所能达到的效果相差百倍以上,比优秀程序员和平庸程序员之间的差别还大。”

“优秀的写作者的RTX就是能让对方明白他的目的,并且像施了魔咒一样去合作。优秀的写作者的邮件能让接受者感兴趣,清晰地知道信息。优秀的写作者写的博客能用一段话击中读者心理,情不自禁点右上角的“分享到朋友圈”……这种效果100个平庸的写作者都达不到。”

“写作者需要的除了文笔,还有逻辑思维、数据分析、麦肯锡金字塔理论、心理学等等几乎所有的知识……”

每个人每天都要阅读微信、朋友圈、新闻、读书、知乎、小视频……关于写作对个人能力的的重要性,我认为怎么强调都不为过。因为广义的写作、演讲和设计,它们有一个共同的关键内核,就是搞清楚你的听众是谁,他们已有哪些信息,缺乏哪些信息,你要以怎么样的顺序来传达你想让对方做的事情。讲得邪乎点,它们都是一种“心理操纵术”。

那怎么才能学好写作(或者演讲,或者设计)呢?

答案无趣但有效:持续写。

反复阅读写出来的文字,毫不吝啬地删除无用的信息,重新再写。

达芬奇说过一句话:“Simplicity is the ultimate form of sophistication(简洁是终极的复杂)”。海明威每天写作之前都会把前一天的稿再改一次。

我也这样做,一开始在自己博客上写水文、在豆瓣写书评,感觉不够。2014年2月,我加入豆瓣专栏计划,需要每周写一篇超过3000字的专栏文章,结束后能获得200元鼓励金。我就像一个缺乏运动的人,强行把自己推入马拉松赛道。

我的前几篇写的很业余,错别字、口水话、病句、缺主语、串主语、一逗到底、唠唠叨叨、层级和顺序不对……那也还是要写。几个月后,20篇文章的专栏完结了,我的文字水平稳定从30分提升到50分,接近及格。

因为专栏内容相对新颖(可能是国内首批系统写“全栈工程师”的思考的专栏),慢慢积累一些读者并每周追看。读者宽容并热情地在评论区给我纠错。

后来,人民邮电出版社的责编在豆瓣上看到了我,就约我写稿。他说我写的东西已经很多,也有一些脉络,可以再整理一下出书。又是一个新的挑战。

先答应再说吧。

写书的过程只能说勉力支撑,因为只有50分的文字水平,却要输出80分的质量。把第一章整理好之后发过去,收到返回的修正稿,变成了另一篇文章。责编很专业,没有吐槽,只是做客观订正。

我羞愧难当,因为痛恨给人添麻烦。我记住修改过的问题种类,文法上字斟句酌,保证同类的不再犯。

因为责编会看出文字上的问题,然后给我修剪枝丫,但保持大树根基稳定就是我自己的责任,对读者的责任。我还买来《麦肯锡教我的写作武器》,更系统地学习写作。

经过好多轮的校对,我终于可以坦然说出,差不多达到基本的标准了。后面的事情我在“我出书了”中也都写了。2015年8月,我的书出版了。我在豆瓣上也慎重给《Web全栈工程师的自我修养》打了4星。

三、

写作成长磕磕碰碰的同时,管理之路也迂回曲折。试着带一段时间团队之后,我在2014年正式成为团队管理者。

当时对于团队管理的职责抱有几个不成熟心态:

管理比写代码更容易掌握,践行起来也更轻松
管理者门槛较低,相较于工程师缺乏核心竞争力,以后跳槽我还是要以工程师身份来定位
我喜欢专注做事情,不适合做管理
在工程师团队中,我要以最强的技术和努力来赢得尊重,我要有能力解决他们都解决不了的问题
因此,从2014-2016虽然也通过努力收获了一些个人成长,但对团队领导来讲其实我是不称职的。

有一个明显不称职的表现就是,每到员工考核期间,我就很纠结痛苦。我不希望有员工拿低于预期的考评,也害怕面对下属沟通面谈,当面对着他说你的绩效低于预期。

我能自律勤奋,但我很难改变自己的观念。最难的是,我甚至不知道自己是否应该改变自己的观念(瞧,这就是为什么改变观念是最难的),还是说退回去做一个还不错的工程师好了。

我在这个观念段位大概停留了两年多,经过断断续续的实践、阅读、观察和自省,我终于升级了。期间纠结和思考过程可以从2014到2016的博客文章中可见一斑。

总之,升级后的我认为:

管理比写代码(或任何一门硬技能)更难于掌握,这是我跟一些技术专家沟通得到的共识。硬技能的学习可以通过读书、培训班,甚至网络视频来学习,然后持续练习,越来越熟练,直到产生一个输出物。这非常简单,只要掌握了学习的方法,几个月就能学习一门硬技能。而管理的学习需要真实观念的转变,这个观念改变可能需要几年时间。
管理的核心观念是“管理者必须善于做有效的决策”。
管理者要注重组织对外的贡献。基于贡献来衡量每个人的绩效。
我开始积极与团队沟通,日常中看到不符合要求的输出,我就会直截了当地说“这不行,达不到基本水准”。

虽然比较严格,但也没有看到团队氛围下降的情况。因为从员工角度来讲,虽然乐于处在一个人际关系融洽的团队中,但更大的述求是加入一个充满专业人士的团队。每人都能从其他人身上学到特定知识,每人表现都是专业的。

我不再担心员工考核,因为它是一个有效的管理工具。有些无法用言语传达到的信息,可以通过绩效考核来传达。而且平时对于低绩效的员工就要做好预期管理,言行一致员工就不会困惑。

四、

2017年,我慢慢成为一个资深的管理者。又一次对工作驾轻就熟时,再次迎来新的挑战——转换岗位,领导腾讯微云UX设计团队。

我喜欢这个挑战,一方面它确实是一个“很大挑战”,受虐症的我无法拒绝。另一方面我一直处于产品流程中偏后的位置,但也对前置的思考很感兴趣。

因此,我梳理了自己面临的挑战:

之前领导的团队都是工程师,而现在的团队由交互设计师和视觉设计师组成。虽然管理的基本法是相通的,但新团队的成员还需要更多熟悉
自己的设计专业能力不够,尤其是在视觉上,无法给到“怎么做”的建议
新的UX设计团队面临比以前更复杂的外部关系
如何帮助下属专业晋升
但我也有我的优势:

能轻松地理解版本管理、多平台特性、开发挑战等“工程”难题,然后管理好风险
参加了三年多设计部的管理会,对设计的“味道”有感觉。或者说品味远远高出实现能力
在UI方案上,我有用户同理心,不只是从“好看”来评判设计
我的演讲呈现能力和写作能力可以提升团队
唔,也不全是坏消息嘛。那就开始做吧!

前半年仍然是勉力支撑(哭),但因为团队都在看着自己,不自信也要自信起来。

工作之余多体验各种APP,收集UI、运营、营收、品牌等方面的案例,进一步提升“产品力”。老婆平时在使用新的APP,或者被活动吸引付费时,我就会在边上观察她的行为。看到精彩处,我还会请求暂停,截图发给我。

总之,我希望自己的专业成长能快速补上权限扩大带来的差距。

慢慢地,有一些“腾讯微云用户体验不错”的口碑了,在自己能影响的范围内,使用我能调用的资源,慢慢地补齐漏洞,提升体验。

但仍然能力有限,有时候会出现仓促出的方案不合理的情况。这时更加如履薄冰,不是怕外部批评我,而是怕连累授权给我的上司,和被我能力所限的团队。加上腾讯微云也是一个有历史包袱的产品,所以仍然离自己心中的理想产品差很多。

团队成长上的挑战同样很大。

我仔细观察每一个人的优缺点,用人所长。于此同时我还要横向去看部门其他设计师的输出,不希望相对独立的产品导致了封闭的专业氛围。这将是接下来花半年到一年重点要解决的。

我对现在这个挑战,还远远没到驾轻就熟的状态,可能还需要两年以上时间来消化,所以有时工作会觉得比前几年加起来还累。

我时常以山本耀司的话给自己打鸡血:

我从不相信什么懒洋洋的自由,我向往的自由是通过勤奋和努力实现更广阔的人生,那样的自由才是珍贵的、有价值的。我相信一万小时定律,我从来不相信天上掉馅饼的灵感和坐等的成就。做一个自由又自律的人,靠势必实现的决心认真地活着。

勤奋和努力只是基础,以大多数人的努力程度之低,还根本谈不上拼天赋。

疲劳和兴奋交替,成就和挫折并存,曲折前行,比轻松混日子更有趣。

五、

回头看,这八年来,我从来都只是“短暂地胜任”自己的工作。每当我觉得能够驾轻就熟地处理目前的日常,就会迎来新的挑战。

想到这一点,我突然悟到一种“佛性”的工作哲学:

每当你能胜任当前的工作,就会迎来更高难度的挑战。

每一个能胜任当前工作岗位的人,都会被提拔。继续胜任,那就继续提拔,直到不能胜任。

因此,不用特别在意自己的头衔、权限和职级,外部的认可是你能力的反馈。你没有被提拔,大概率是因为还不胜任当前工作。如果完全胜任还没有被安排更有挑战的工作,要么自己找事情做,要么跳槽转岗。

反过来说,自知自己能力还达不到岗位要求也不用担心,不胜任是常态,以胜任为目标就好啦。

对于职场马拉松来说,心态放松,保持作息,持续养成好的习惯,学习好的方法和观念,这才是最重要的。

我写字的地方迁移到公众号啦~欢迎关注我的公众号:余果专栏

基本步骤

  • hexo 文档:https://hexo.io/zh-cn/docs/
  • 安装git、node、npm、hexo、next主题
  • 在github建立wxquare.github.io仓库,两个branch,分别为master和hexo
  • 使用markdown在hexo branch 写文章,hexo generate生成静态文件,并通过hexo deploy 部署到远端
  • 申请域名wxquare.top,绑定wxquare.github.io
  • https://wxquare.github.io/

写文章发布blog的流程

  • 在hexo branch /source/_posts 下使用markdown写文章
  • 使用hexo genergate 生成静态文件
  • hexo server 查看本地效果
  • hexo deploy 到远端
  • 提交修改文件到hexo
1
2
3
4
5
npm i -g hexo  安装hexo
hexo init 初始化
hexo generate 生成静态网页
hexo server 启动服务器 (浏览器输入http://localhost:4000/验证是否正确。)
hexo deploy 部署到远端 (wxquare.github.io)

hexo 配置

  1. 修改站点配置文件_config.yml,使得能将本地博客部署到github上
    1
    2
    3
    4
    deploy:
    type: git
    repo: https://github.com/wxquare/wxquare.github.io.git
    branch: master

next主题 配置

  1. 主题设置和修改。hexo初始化默认的主题是landscape,https://hexo.io/themes/提供了许多的主题,根据喜好为博客的主题,主题的文档提供了使用方法,设置相应的参数,调整为自己喜欢的格式。我这里选择的Next主题
  2. 安装next主题:https://theme-next.js.org/docs/getting-started/
  3. 主题设置:https://theme-next.js.org/docs/theme-settings/

其它

  1. 增加分类
  2. hexo 增加支持markdown公式:http://stevenshi.me/2017/06/26/hexo-insert-formula/
  3. 博客中的图片,将图片放在hexo分支的source/images目录下面,markdown和blog中均可以看到
  4. Hexo博客Next主题添加统计文章阅读量功能:https://bjtu-hxs.github.io/2018/06/12/leancloud-config/

CPU任务调度,进程/线程/协程

  1. 进程和线程的区别,了解协程吗?CPU调度,数据共享。
  2. 复杂系统中通常融合了多进程编程,多线程编程,协程编程
  3. 进程之间怎么通信,线程通信通信,协程怎么通信
  4. 进程之间怎么同步(信号量,自旋锁,屏障),线程之间怎么同步(锁),协程怎么同步。进程之间通过共享内存、管道、消息队列消息队列等方式通信,通过信号和信号量进行同步。线程在进程内部,全部变量时共享的,通过锁机制来同步。
  5. 死锁:产生的四个条件、四个解决方法,死锁检测
  6. 守护进程,linux系统编程实现守护进程
  7. 在Linux上,对于多进程,子进程继承了父进程的下列哪些?堆栈、文件描述符、进程组、会话、环境变量、共享内存
  8. 僵尸进程和孤儿进程。孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。僵尸进程:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。
  9. 进程的状态。
    • TASK_RUNNING(运行态):进bai程是可执行du的;或者正在执行,zhi或者在运行队列中等待执行。
    • TASK_INTERRUPTIBLE(可中断睡眠态):进程被阻塞,等待某些条件的完成。一旦完成这些条件,内核就会将该进程的状态设置为运行态。
    • TASK_UNINTERRUPTIBLE(不可中断睡眠态):进程被阻塞,等待某些条件的完成。与可中断睡眠态不同的是,该状态进程不可被信号唤醒。
    • TASK_ZOMBIE(僵死态):该进程已经结束,但是其父进程还没有将其回收。
    • TASK_STOP(终止态):进程停止执行。通常进程在收到SIGSTOP、SIGTTIN、SIGTTOU等信号的时候会进入该状态。
  10. linux的CFS调度机制是什么?时间片/policy(进程类别)/priority(优先级)/counter。linux的任务调度机制是什么?在每个进程的task_struct结构中有以下四项:policy、priority、counter、rt_priority。这四项是选择进程的依据。其中,policy是进程的调度策略,用来区分实时进程和普通进程,实时进程优先于普通进程运行;priority是进程(包括实时和普通)的静态优先级;counter是进程剩余的时间片,它的起始值就是priority的值;由于counter在后面计算一个处于可运行状态的进程值得运行的程度goodness时起重要作用,因此,counter 也可以看作是进程的动态优先级。rt_priority是实时进程特有的,用于实时进程间的选择。 Linux用函数goodness()来衡量一个处于可运行状态的进程值得运行的程度。该函数综合了以上提到的四项,还结合了一些其他的因素,给每个处于可运行状态的进程赋予一个权值(weight),调度程序以这个权值作为选择进程的唯一依据。
  11. goroutine的GPM,没有时间片和优先级的概念,但也支持“抢占式调度”。 goroutine的主要状态grunnable、grunning、gwaiting
  12. 线程的状态
    • runnable
    • running
    • blocked
    • dead
  13. 进程、线程与协程的区别
  14. 操作系统写时复制:https://juejin.cn/post/6844903702373859335
  15. 操作系统为什么设计用户态和内核态,用户态和内核态的权限不同?怎么解决IO频繁发生内核和用户态的态的切换(缓存)?
  16. select、epoll的监听回调机制,红黑树?
  17. 从一道面试题谈linux下fork的运行机制
  18. malloc分配多少内存:http://fallincode.com/blog/2020/01/malloc%e6%9c%80%e5%a4%9a%e8%83%bd%e5%88%86%e9%85%8d%e5%a4%9a%e5%b0%91%e5%86%85%e5%ad%98/

存储系统,内存和存储

  1. 寄存器、缓存cache、内存和磁盘
  2. 可执行文件的空间结构,进程的空间结构(虚拟地址空间,栈,堆,未初始化变量,初始化区,代码)
  3. 查看进程使用的资源,top,ps,cat /proc/pid/status
  4. 进程的虚拟内存机制(虚拟地址-页表-物理地址)。Linux虚拟内存的实现需要6种机制的支持:地址映射机制、内存分配回收机制、缓存和刷新机制、请求页机制、交换机制和内存共享机制,内存管理程序通过映射机制把用户程序的逻辑地址映射到物理地址。当用户程序运行时,如果发现程序中要用的虚地址没有对应的物理内存,就发出了请求页要求。如果有空闲的内存可供分配,就请求分配内存(于是用到了内存的分配和回收),并把正在使用的物理页记录在缓存中(使用了缓存机制)。如果没有足够的内存可供分配,那么就调用交换机制;腾出一部分内存。另外,在地址映射中要通过TLB(翻译后援存储器)来寻找物理页;交换机制中也要用到交换缓存,并且把物理页内容交换到交换文件中,也要修改页表来映射文件地址。
  5. 操作系统内存分配算法常用缓存置换算法(FIFO,LRU,LFU),LRU算法的实现和优化?
  6. Linux系统原理之文件系统(磁盘、分区、文件系统、inode表、data block)
  7. 在linux执行ls上实际发生了什么
  8. CPU寻址过程,tlb,cache miss.
  9. 栈和堆的区别

系统编程以及其它注意事项

  1. 使用过哪些进程间通讯机制,并详细说明,linux进程之间的通信7种方式
  2. 内核函数、系统调用、库函数/API,strace系统调用追踪调试
  3. coredump文件产生?内存访问越界、野指针、堆栈溢出等等
  4. fork 和 vfork,exec,system(进程的用户空间是在执行系统调用的fork时创建的,基于写时复制的原理,子进程创建的时候继承了父进程的用户空间,仅仅是mm_struc结构的建立、vm_area_struct结构的建立以及页目录和页表的建立,并没有真正地复制一个物理页面,这也是为什么Linux内核能迅速地创建进程的原因之一。)写时复制(Copy-on-write)是一种可以推迟甚至免除拷贝数据的技术。内核此时并不复制整个进程空间,而是让父进程和子进程共享同一个拷贝。只有在需要写入的时候,数据才会被复制,从而使各个进程拥有各自的拷贝。也就是说,资源的复制只有在需要写入的时候才进行,在此之前,以只读方式共享。这种技术使地址空间上的页的拷贝被推迟到实际发生写入的时候。有时共享页根本不会被写入,例如,fork()后立即调用exec(),就无需复制父进程的页了。fork()的实际开销就是复制父进程的页表以及给子进程创建唯一的PCB。这种优化可以避免拷贝大量根本就不会使用的数据
  5. 锁?互斥锁的属性设置、多进程共享内存的使用、多线程的使用互斥锁、pshaed和type设置。使用互斥量和条件变脸实现互斥锁
  6. 共享内存的同步机制,使用信号量,无锁数据结构
  7. 多线程里一个线程sleep,实质上是在干嘛,忙等还是闲等。?
  8. exit()函数与_exit()函数最大的区别就在于exit()函数在调用exit系统调用之前要检查文件的打开情况,把文件缓冲区中的内容写回文件,就是”清理I/O缓冲”。
  9. select/epoll https://www.cnblogs.com/anker/p/3265058.html
  • select 内核态和用户态重复拷贝
  • select 需要遍历遍历查找就绪的socket
  • select 有数量限制1024
  • epoll 注册时写进内核
  • epoll_wait 返回就绪的事件

网络编程

  1. 简单了解C语言的socket编程api。socket,bind,listen,accept,connect,read/write.

  2. Linux下socket的五种I/O 模式,同步阻塞、同步非阻塞、同步I/O复用、异步I/O、信号驱动I/O

  3. Linux套接字和I/O模型

  4. select和epoll的区别

  5. 什么是I/O 复用?关于I/O多路复用(又被称为“事件驱动”),首先要理解的是,操作系统为你提供了一个功能,当你的某个socket可读或者可写的时候,它可以给你一个通知。这样当配合非阻塞的socket使用时,只有当系统通知我哪个描述符可读了,我才去执行read操作,可以保证每次read都能读到有效数据而不做纯返回-1和EAGAIN的无用功。写操作类似。操作系统的这个功能通过select/poll/epoll/kqueue之类的系统调用函数来使用,这些函数都可以同时监视多个描述符的读写就绪状况,这样,多个描述符的I/O操作都能在一个线程内并发交替地顺序完成,这就叫I/O多路复用,这里的“复用”指的是复用同一个线程。

  6. 网络分析工具。ping/tcpdump/netstat/lsof

其它问题

  1. 计算机中浮点数表示方法,以及浮点数转换中精度缺失的问题

  在2018年的CVPR上SiameseRPN模型被提出,它宣称在单目标跟踪问题上做到了state-of-the-art,能同时兼顾精度(accuracy)和速度(efficiency)。在这之后,很快又在ECCV上发表了DaSiamRPN模型,它在SiameseRPN基础进一步提升了追踪的性能。SiameseRPN不是一簇而就的,它的设计思想来源于SiameseFc,并引入物体检测领域的区域推荐网络(RPN),通过网络回归避免了多尺度测试,同时得到更加精准的目标框和目标的位置。实际使用中发现DaSiamRPN相比传统的KCF效果直观感受确实精度有较大提升,在普通pc无GPU环境上大概是10.6fps。这里主要结合SimeseRPN的论文DaSiamRPN的代码帮助了解SimeseRPN的模型结构以及DaSiamRPN的运行过程。

SiameseRPN模型

  Siamese-RPN本质上是组合网络模型,它包括用于特征提取的Siamese网络和生成候选区域的RPN网络。
  Siamese特征提取网络:它目前在追踪领域使用比较多,包括模板分支(template branch)和检测分支(detection branch),它们都是经过裁剪的AlexNet卷积网络,用于提取图像的特征。两个分支网络参数和权重值完全相同,只是输入不同,模板分支输入模板帧中的目标部分(target patch),检测分支输入当前需要追踪的帧的区域(target patch)。
  RPN(region proposal subnetwork)候选区域生成网络:它包括的分类(classification)和回归(regression)两个分支。这里有个重要的锚点(anchor),就是通过RPN对每个锚点上的k个不同宽度和高度的矩形分类和回归,得到感兴趣区域。每个anhcor box要分前景和背景,所以cls=2k;而每个anchor box都有[x, y, w, h]对应4个偏移量,所以reg=4k。

SiameseRPN模型

  因此设模板分支输入为$z$维度为(127,127,3),首先通过Siamese网络特征提取得到$ψ(z)$维度为(6,6,256),然后再经历卷积分别的到$[ψ(z)]{cls}$和$[ψ(z)]{res}$。检测分支输入为$x$,$ψ(x)$为Siamese特征提取网路的输出,以$[ψ(z)]{cls}$和$[ψ(z)]{res}$为核卷积得到最终的SiameseRPN的输出,$*$表示卷积运算。
$$A_{w×h×2k}^{cls} = [ψ(x)]{cls} * [ψ(z)]{cls}$$

$$A_{w×h×4k}^{res} = [ψ(x)]{res} * [ψ(z)]{res}$$

DaSiamRPN视频追踪的过程

  DaSiamRPN做视频目标追踪,DaSiamRPN相比SiameseRPN做了进一步的优化,例如训练时引入采样策略控制不平衡的样本分布,设计了一种distractor-aware模块执行增量学习等。结合官方的https://github.com/foolwood/DaSiamRPN 中的例子,很容易将demo运行起来。需要注意的是github上的代码需要gpu运行环境,如果要在无gpu机器上运行DaSiamRPN的demo需要将有关cuda代码去掉。例如将将net.eval().cuda()换成net.eval()。DaSiamRPN的运行包含两个步骤:

  1. 初始化。输入模板帧,得到$[ψ(z)]{cls}$和$[ψ(z)]{res}$两个用于卷积的核。
  2. 追踪。将待追踪帧输入到模型,得到每个候选区域的score和偏移delta。从候选区域中选出分数最高的候选区域proposal。

初始化

  1. 输出模板图片im,模板图片中目标位置target_pos,目标大小target_size,使用get_subwindow_tracking函数裁剪目标区域临近部分(target patch),并将裁剪得到图片resize到宽和高为127的图片。
  2. 将模板目标区域裁剪好的视频输入网络模型的模板分支(template branch),得到$[ψ(z)]{cls}$和$[ψ(z)]{res}$
  3. 使用generate_anchor函数产生anchers,其大小为$(271-127)/8+1=19,19*19*5=1805$,anchor的维度为(4,1805),这表示会有1805个候选区域,偏移量$d_x,d_y,d_w,d_h$

追踪

  1. 输入追踪的图片im,基于上一帧的target_pos和目标的大小位置target_size,在图片中裁剪部分区域并将该区域resize到271*271得到x_crop。
  2. 将x_crop输入网络的检测分支(detection branch)得到对所有anchor进行分类和回归得到delta和score。
  3. 根据delta获取细化后的候选区域(refinement coordinates)
    1
    2
    3
    4
    5
    # generate the refined top K proposals
    delta[0, :] = delta[0, :] * p.anchor[:, 2] + p.anchor[:, 0] #x
    delta[1, :] = delta[1, :] * p.anchor[:, 3] + p.anchor[:, 1] #y
    delta[2, :] = np.exp(delta[2, :]) * p.anchor[:, 2] #w
    delta[3, :] = np.exp(delta[3, :]) * p.anchor[:, 3] #h
  4. 结合scale penalty、ration penalty、cosine window调整每个候选区域score中每个候选区域的分数,选出分数最大的候选区域best_pscore_id.
    1
    2
    3
    4
    5
    6
    7
    8
    # size penalty
    s_c = change(sz(delta[2, :], delta[3, :]) / sz_wh(target_sz)) # scale penalty
    r_c = change((target_sz[0] / target_sz[1]) / (delta[2, :] / delta[3, :])) # ratio penalty
    penalty = np.exp(-(r_c * s_c - 1.) * p.penalty_k)
    pscore = penalty * score
    # window float
    pscore = pscore * (1 - p.window_influence) + window * p.window_influence
    best_pscore_id = np.argmax(pscore)
  5. 计算出当前帧目标的位置target_pos和target_size。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    target = delta[:, best_pscore_id] / scale_z
    target_sz = target_sz / scale_z

    lr = penalty[best_pscore_id] * score[best_pscore_id] * p.lr

    res_x = target_pos[0] + target[0]
    res_y = target_pos[1] + target[1]
    res_w = target_sz[0] * (1 - lr) + target[2] * lr
    res_h = target_sz[1] * (1 - lr) + target[3] * lr

    target_pos = np.array([res_x, res_y])
    target_sz = np.array([res_w, res_h])

参考:

  1. https://zhuanlan.zhihu.com/p/37856765
  2. https://github.com/foolwood/DaSiamRPN
  3. http://openaccess.thecvf.com/content_cvpr_2018/papers/Li_High_Performance_Visual_CVPR_2018_paper.pdf

  在《初识TVM,相比于tensorflow的2倍性能提升》之后,最近花了一点业余时间了解TVM及其周边,并进行相应的性能测试。整体感受是计算优化(GEMM)是非常繁杂的工程工作,需要花费大量的时间和精力才能有比较好的效果。numpy非常优秀,大矩阵乘法硬件利用率在90%以上。TVM在GEMM优化上能实现和numpy相当的效果,重要的是它能大大简化工作量。参考了一些文章,这里简单罗列了几个知识点和测试数据。

  1. 怎么评估硬件的理论性能?浮点峰值?
  2. 简单测试一下numpy的性能数据,硬件利用率
  3. 怎么做GEMM优化?
  4. TVM怎么做GEMM的优化?及其与numpy性能的比较

怎么评估硬件的计算性能

  对于性能优化来说,了解硬件的性能指标是非常有必要的。在Linux系统上可以通过/proc/cpuinfo文件看看机器的配置。比如CPU主频、CPU核数core、cache大小、是否支持向量指令SSE、AVX2、AVX512等,这些对于计算性能有非常大的影响。浮点峰值那些事儿。通常我们使用浮点计算能力来衡量硬件的性能,对于多核服务器来说,频率为2.4G,支持AVX2,FMA向量指令,单核性能如下:
对于float32理论峰值为2.4G * (8+8) * 2 = 76.8 GFLOPS
对于float64理论峰值为2.4G * (4+4) * 2 = 38.4 GFLOPS

测试numpy GEMM硬件利用率

  numpy非常优秀,我们通过矩阵乘法了解其性能数据。测试机器为一台多核的服务器,主频是2.4G,支持FMA和AVX2向量指令。测试了不同size矩阵相乘的性能数据。分别测试了单核和四核状态下对float32和float64的不同size(32,128,1024,2048等)矩阵相乘的性能数据。测试结果显示numpy在大矩阵相乘中,硬件利用率大概在90%左右。

name | 32 | 128|1024|2048|4096|10240|硬件利用率|
-|-|-|
单核float32|1.82|36.16|67.99|67.94|68.88|69.88|91.0%
单核float64|1.67|19.49|35.56|35.40|36.11|36.90|96.1%
四核float32|6.6|52.2|225.42|246.2|244.2|256.0|83.8%
四核float64|5.56|37.62|116.42|120.39|127.03|141.15|91.9%
测试代码

怎么优化GEMM?

  通用矩阵乘(GEMM)是计算领域非常基础且核心的工作,目前已有大量的工作,这里就不赘述了。大体上通过分块来减少访存次数、存储顺序、提高cache命中率、利用寄存器提高效率、利用SSE等向量指令提高计算效率等方法。https://github.com/flame/how-to-optimize-gemm/wiki 一步一步详细介绍了GEMM优化的过程,这里在此基础上增加FMA指令的使用,测试了其在1024*1204矩阵相乘的硬件利用率:

name | 64 | 256 |512|1024|硬件利用率|主要优化点|
-|-|-|
MMult0|1.51|0.79|0.66|0.65|1.69%|base
MMult_1x4_5|2.15|1.08|0.72|0.716|2.6%|一次计算1x4个数
MMult_1x4_9|4.90|3.15|3.10|3.14|8.18%|1x4,寄存器
MMult_4x4_5|2.76|1.53|1.26|1.26|3.28%|一次计算4x4个数
MMult_4x4_9|5.19|2.92|2.88|2.87|7.47%|4x4,寄存器
MMult_4x4_10|5.95|4.16|4.04|4.01|10.4%|4x4,寄存器,SSE
MMult_4x4_10_1|10.0|6.6|6.35|6.4|16.7%|4x4,寄存器,FMA
MMult_4x4_11_1|14.5|8.95|7.16|7.08|18.4%|4x4,寄存器,FMA,分块(缓存)
MMult_4x4_15_1|11.3|11.6|11.7|11.7|30.4%|4x4,寄存器,FMA,分块,内存顺序

测试代码

TVM GEMM优化与numpy性能比较

  TVM官网上有关于其针对GEMM的优化的schedule,这里也不赘述了,感兴趣的可以参考后面的参考文章进一步学习,这里测试了在1024*1024矩阵乘法的效率以及其和numpy的比较,可以看出TVM在简单编码的基础上能达到和numpy相当的性能。

| TVM运行时间 | numpy运行时间 |
-|-|-|
baseline|2.49s|0.0135s
blocking|1.73s|0.012s
vectorization|0.411s|0.0117s
loop permutaion|0.104s|0.0116s
packing|0.0987s|0.0103s
write_cache|0.0926s|0.01158s
parallel|0.018s|0.012s
auto-tvm|0.014s|0.0112s
每个阶段测试代码

参考学习链接:
1、浮点峰值那些事儿https://zhuanlan.zhihu.com/p/28226956
2、通用矩阵乘(GEMM)优化算法,https://jackwish.net/gemm-optimization.html
3、如何利用TVM快速实现超越Numpy(MKL)的GEMM。https://zhuanlan.zhihu.com/p/75203171
4、tutorial:https://docs.tvm.ai/tutorials/optimize/opt_gemm.html
5、d2ltvm:http://tvm.d2l.ai/chapter_cpu_schedules/index.html
6、https://github.com/flame/how-to-optimize-gemm

0%