余滴滴 A Programmer 坚持是一种刻意的练习,
不断寻找缺点突破缺点的过程,而不是重复做某件事情。

GORM实现MySQL事务

禁用默认事务

GORM 事务默认是开启的。为了确保数据一致性,GORM 会在事务里执行写入操作(增删改)。如果对数据一致性要求不高的话,可以在初始化时禁用它,性能将提升大约 30%。一般不推荐禁用。

// 全局禁用
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
    SkipDefaultTransaction: true,
})

自动事务

通过 db.Transaction 函数实现事务,如果闭包函数返回错误,则回滚事务。

db.Transaction(func(tx *gorm.DB) error {
    // 在事务中执行一些 db 操作(从这里开始,您应该使用 'tx' 而不是 'db')
    if err := tx.Create(&User{Name: "Lili"}).Error; err != nil {
        // 返回任何错误都会回滚事务
        return err
    }
    if err := tx.Create(&User{Name: "xiaoming"}).Error; err != nil {
        return err
    }
    // 返回 nil 提交事务
    return nil
})

手动事务

在开发中经常需要数据库事务来保证多个数据库写操作的原子性。例如电商系统中的扣减库存和保存订单。

GORM 事务用法:

// 开启事务
tx := db.Begin()

// 在事务中执行数据库操作,使用的是 tx 变量,不是 db。
// 库存减一
// 等价于: UPDATE `goods` SET `stock` = stock - 1 WHERE `goods`.`id` = '2' and stock > 0
// RowsAffected 用于返回 sql 执行后影响的行数
rowsAffected := tx.Model(&goods).Where("stock > 0").Update("stock", gorm.Expr("stock - 1")).RowsAffected
if rowsAffected == 0 {
    // 如果更新库存操作,返回影响行数为 0,说明没有库存了,结束下单流程
    // 这里回滚作用不大,因为前面没成功执行什么数据库更新操作,也没什么数据需要回滚。
    // 这里就是举个例子,事务中可以执行多个 sql 语句,错误了可以回滚事务
    tx.Rollback()
    return
}

err := tx.Create(保存订单).Error
// 保存订单失败,则回滚事务
if err != nil {
    tx.Rollback()
} else {
    tx.Commit()
}

嵌套事务

GORM 支持嵌套事务,可以回滚较大事务内执行的一部分操作,例如:

db.Transaction(func(tx *gorm.DB) error {
    tx.Create(&user1)
    tx.Transaction(func(tx2 *gorm.DB) error {
        tx2.Create(&user2)
        return errors.New("rollback user2") // Rollback user2
    })
    tx.Transaction(func(tx2 *gorm.DB) error {
        tx2.Create(&user3)
        return nil
    })
    return nil
})
// Commit user1, user3

SavePoint、RollbackTo

GORM 提供了 SavePointRollbackTo 来提供保存点以及回滚至保存点,例如:

tx := db.Begin()
tx.Create(&user1)
tx.SavePoint("sp1")
tx.Create(&user2)
tx.RollbackTo("sp1") // Rollback user2
tx.Commit()         // Commit user1

这里 rollback 到了 sp1 的位置,也就是说,数据库中只存了 user1 这条数据。

小结

总的来说 GORM 事务的实现非常简单,Go 语言操作 MySQL 事务主要用到了三个函数:

  • Db.Begin() 开始事务
  • Db.Commit() 提交事务
  • Db.Rollback() 回滚事务

尝试用cursor阅读开源代码gorm v2

1.让cursor绘制gorm v2中query查询的时序图

使用CodeBase的tip要求cursor基于项目库中全部的go文件作为上下文进行回答, 然后要求它绘制时序图。

根据cursor回复情况,可以看出cursor的大概处理思路:首先是文件搜索+片段匹配的前置步骤,然后再生成回答。这是一种典型的RAG处理思路。

我是这样开始问的

@CodeBase  *.go 以query为例体现gorm查询的全部过程,给我时序图。需要包括具体的文件路径和函数名。

省略中间的多次会话沟通。。。

经过多次来回沟通,最终如下

@startuml
participant "应用代码" as App
participant "gorm.Open()\n(gorm.go)" as Open
participant "callbacks.go\ninitializeCallbacks()" as InitCallbacks
participant "processor\n(callbacks.go)" as Processor
participant "gorm.DB\n(finisher_api.go)" as DB  
participant "Statement\n(statement.go)" as Statement
participant "Query回调\n(callbacks/query.go)" as QueryCallback
participant "BuildQuerySQL\n(callbacks/query.go)" as BuildQuerySQL
participant "Clause处理\n(clause/*)" as Clause
participant "数据库驱动" as Driver
participant "gorm.Scan\n(scan.go)" as Scanner

== 初始化阶段 ==
App -> Open: Open(dialector, config)
activate Open

Open -> InitCallbacks: initializeCallbacks(db)
activate InitCallbacks
InitCallbacks -> InitCallbacks: 创建processors map
InitCallbacks -> QueryCallback: 注册Query回调
note right: 将Query/Preload/AfterQuery\n函数存入processor.fns切片中
InitCallbacks --> Open: 返回callbacks实例
deactivate InitCallbacks

Open --> App: 返回初始化完成的*gorm.DB
deactivate Open

== 查询阶段 ==
App -> DB: Find(&users)
activate DB

DB -> Statement: 初始化Statement
activate Statement
Statement -> Statement: 设置Dest为&users
deactivate Statement

DB -> Processor: 执行回调链
activate Processor
note right: 遍历processor.fns切片\n依次执行回调函数

Processor -> QueryCallback: Query(db)
activate QueryCallback

QueryCallback -> BuildQuerySQL: BuildQuerySQL(db)
BuildQuerySQL --> QueryCallback: 返回SQL

QueryCallback -> Driver: QueryContext执行SQL
activate Driver
Driver --> QueryCallback: 返回结果集(rows)
deactivate Driver

QueryCallback -> Scanner: Scan(rows, db, 0)
activate Scanner
note right: 将rows扫描到\ndb.Statement.Dest(&users)中
Scanner -> Statement: 设置扫描结果到Dest
Statement --> Scanner: 扫描完成
Scanner --> QueryCallback: 扫描完成
deactivate Scanner

QueryCallback --> Processor: Query回调完成
deactivate QueryCallback

Processor -> Processor: 执行Preload回调
Processor -> Processor: 执行AfterQuery回调
deactivate Processor

DB --> App: 返回查询结果
deactivate DB
@enduml

然后让它再给个文字版描述

好的,我来详细描述 GORM 的完整执行过程:

### 1. 初始化阶段
- **gorm.Open() 初始化**
  -`gorm.go` 中调用 Open() 创建数据库连接
  - 初始化 DB 配置和 Statement
  - 调用 initializeCallbacks() 初始化回调系统

- **回调注册**
  -`callbacks.go` 中创建 processors map
  - 通过 RegisterDefaultCallbacks 注册默认回调:
    ```go
    queryCallback := db.Callback().Query()
    queryCallback.Register("gorm:query", Query)
    queryCallback.Register("gorm:preload", Preload)
    queryCallback.Register("gorm:after_query", AfterQuery)
    ```

### 2. 查询准备阶段
- **创建查询会话**
  -`finisher_api.go` 中调用 getInstance() 创建新的 DB 实例
  - 初始化 Statement 结构体
  - 设置查询的 Model 和 Dest

- **构建查询条件**
  - 添加基础查询子句(SELECT/FROM等)
  - 处理用户传入的查询条件
  - 设置查询相关的配置(如 Preload)

### 3. 查询执行阶段
- **Query 回调执行**
  -`callbacks/query.go` 中执行 Query 函数
  - 调用 BuildQuerySQL 构建完整 SQL 语句
  - 通过数据库驱动执行实际查询
  - 使用 gorm.Scan 将结果扫描到目标结构体

- **Preload 关联处理**
  - 执行预加载回调处理关联数据
  - 为每个关联执行额外的查询
  - 将关联数据映射到主结构体

### 4. 后处理阶段
- **AfterQuery 回调执行**
  - 清理查询状态(如 joins)
  - 执行用户定义的 AfterFind 钩子
  - 完成最终的数据处理

- **返回结果**
  - 检查错误状态
  - 返回查询结果给应用层

整个过程是一个链式调用的流程,通过回调系统实现了高度的可扩展性,允许在查询的不同阶段插入自定义逻辑。每个阶段都有明确的职责划分,共同协作完成数据库查询操作。

2. 最终产出的时序图

gorm进行query的时序图 基本能够很好的辅助一个初级研发看懂gorm v2的核心执行逻辑,主要是回调注册和回调执行的部分。

3. 试用注意

1.注册并登录成功后,只能试用min(2周,150次询问),达到限制后20刀每月

感慨

或许程序员和产品经理可以合并为1个岗位了。。。

分表后数据倾斜问题:手动调整分表方案

1. 业务背景

类似Dexx的交易工具需要抓取和存储dex的大量交易对产生的大量交易数据,交易对数量可达数百万,区块中交易笔数每日可达百万级。 如何保证每个交易对的数据存储和查询效率,尤其是高频交易对(如ETH/USDT等)成为了亟待解决的问题。

用户侧主要有两种查询:

  1. 指定交易对:用户查询某个交易对最近的n笔交易。对应[最新成交]的功能模块
  2. 指定交易对和指定钱包地址:用户查询某个交易对某个钱包地址最近的n笔交易。用于发现聪明钱

目前1年的交易数量约10亿数据,使用如下的分表方案

2. 当前方案:基于交易对地址路由的分表方案

现有的分表策略中,交易对的交易记录是通过对交易对地址(PoolAddress)进行哈希计算,然后将哈希值对256取余(PoolAddress % 256),来决定该交易对的数据存储在trade_0trade_255之间的256个分表中的一个。 分表路由规则:(PoolAddress(交易对地址字符串) % 256),计算结果决定该交易对的数据存储在trade_0trade_255的哪一个分表。

当前查询语句

  1. 指定交易对where 交易对=?
  2. 指定交易对和指定钱包地址where 交易对=? And 钱包地址=?

3. 当前问题:热点交易对导致分表数据不均衡

尽管分表方案能将大部分交易对的交易记录均匀分布到256个分表中,但热点交易对(如ETH/USDT)的查询频率和交易量可能会大幅度高于其他交易对。这样的数据分布依然可能造成一些分表(例如trade_0)的负载过重,导致查询延迟增加,系统性能下降。

问题分析:

  • 热点交易对的查询压力:存储热门交易对的分表数据量远超过其他分表,单表可达千万,导致查询效率低下。同时热门交易对的查询频率也高,压力更大。累的累死。。。
  • 冷门交易对的资源浪费:与热门交易对相比,冷门交易对几乎没有查询需求,对应的分表(例如trade_100)几乎没有负载,导致计算和存储资源的浪费。闲的闲死。。。

4. 解决方案:手动调整路由分表并迁移数据

为了优化查询性能和数据分布,提出手动调整分表路由的方案,针对热门交易对进行精确的手动调整,而无需进行二次分表。

4.1 手动调整分表路由

针对热门交易对(例如ETH/USDT),我们可以选择通过手动调整路由来优化分表分布。即将这些热门交易对的交易记录迁移到新的分表中,从而减轻原有分表(如trade_0)的查询压力。具体过程如下:

执行思路

识别热点交易对,规划新的分表-> 入库双写+数据迁移 -> 查旧表改为查新表

4.2 实施过程

为确保迁移过程中的数据一致性和查询不中断,我们采用双写策略来实现平滑迁移。迁移的具体步骤如下:

  1. 启用双写:修改爬虫入库逻辑,确保所有新的交易记录同时写入旧的分表(例如trade_0)和新的分表(例如trade_256)。这样可以确保新交易记录在新分表不丢失。

  2. 迁移历史数据:将热点交易对的历史交易记录从原有分表(如trade_0)迁移到新的分表(如trade_256)。编写脚本,分批将历史数据迁移到新分表。

  3. 确认数据一致性:验证新旧分表中的数据一致,避免数据丢失或重复。

  4. 切换查询路由:历史数据迁移完成后,将查询路由切换到新的分表。此时,所有关于ETH/USDT的查询将直接访问trade_256,而不再访问原分表。

  5. 停止双写:在确认迁移完成并且新分表已经承载查询流量后,停止双写模式。此时,所有数据都只写入新的分表。

  6. 清理旧数据:一旦确认新的分表完全接管了查询请求,可以将旧分表中的相关数据删除,释放存储资源。

5. 缺点与挑战

  • 操作复杂性:手动调整分表路由和数据迁移过程需要精准的计划和执行。
  • 数据一致性问题:双写和数据迁移期间,必须确保新旧表的数据一致性,避免因网络问题或系统故障造成的数据丢失或重复。

6. 总结

通过手动调整路由分表数据迁移,可以有效解决由于热点交易对造成的数据倾斜问题。 相比于复杂的二次分表方案,这种方法在操作上更为简单和灵活,并能高效应对去中心化交易所中高频查询的挑战。

uniswap计算实际储备量

根据L,sqrt_price_x96,min-max-tick 计算出当前的实际储备量。同时计算当前价格。

slot0返回的

  • sqrt_price_x96精确
  • tick:这个不精确,只有跨越tick才回记录一下

对应的池子 https://app.uniswap.org/positions/v3/ethereum_sepolia/30563

参考资料

  1. https://blog.uniswap.org/uniswap-v3-math-primer
  2. Liquidity Math in Uniswap v3 by Atis Elsts

计算结果和显示流动性一致

群讨论

_config.yml

import math


def tick_to_price(tick):
    """将tick值转换为价格"""
    return 1.0001 ** tick


def sqrt_price_x96_to_price(sqrt_price_x96):
    """将sqrtPriceX96转换为价格P"""
    # Convert Q64.96 fixed-point number to float and square it to get the price
    sqrt_price = sqrt_price_x96 / (2 ** 96)
    price = sqrt_price ** 2

    # 单独打印价格
    # Adjust for token decimals
    adjusted_decimal_price = price * (10 ** decimals_x) / (10 ** decimals_y)
    print(f'price内部使用价格:{price:.20f}')
    print(f'adjusted_decimal_price使用精度调整后的价格:{adjusted_decimal_price:.20f}')
    reverted_price = 1/adjusted_decimal_price
    print(f"reverted_price(对应UI上的价格10000)(reverted_price=1/adjusted_decimal_price): {reverted_price:.20f} ")  # xxx token per ETH.
    return price


def calculate_real_reserves(L, sqrt_price_x96, lower_tick, upper_tick):
    # 将tick转换为价格
    P = sqrt_price_x96_to_price(sqrt_price_x96)
    pa = tick_to_price(lower_tick)
    pb = tick_to_price(upper_tick)

    # 根据当前价格和流动性计算实际储备量
    if P <= pa:
        # 当前价格低于下界;所有流动性都提供x资产
        x_real = L * (math.sqrt(pb) - math.sqrt(P)) / (math.sqrt(P) * math.sqrt(pb))
        y_real = 0
    elif P >= pb:
        # 当前价格高于上界;所有流动性都提供y资产
        x_real = 0
        y_real = L * (math.sqrt(P) - math.sqrt(pa))
    else:
        # 当前价格位于范围内;流动性同时提供x和y资产
        x_real = L * (math.sqrt(pb) - math.sqrt(P)) / (math.sqrt(P) * math.sqrt(pb))
        y_real = L * (math.sqrt(P) - math.sqrt(pa))

    return x_real, y_real


def calculate_real_reserves_with_decimals(L, sqrt_price_x96, lower_tick, upper_tick, decimals_x=18, decimals_y=18):
    # 将tick转换为价格
    P = sqrt_price_x96_to_price(sqrt_price_x96)
    pa = tick_to_price(lower_tick)
    pb = tick_to_price(upper_tick)

    # 根据当前价格和流动性计算实际储备量
    if P <= pa:
        # 当前价格低于下界;所有流动性都提供x资产
        x_real = L * (math.sqrt(pb) - math.sqrt(P)) / (math.sqrt(P) * math.sqrt(pb))
        y_real = 0
    elif P >= pb:
        # 当前价格高于上界;所有流动性都提供y资产
        x_real = 0
        y_real = L * (math.sqrt(P) - math.sqrt(pa))
    else:
        # 当前价格位于范围内;流动性同时提供x和y资产
        x_real = L * (math.sqrt(pb) - math.sqrt(P)) / (math.sqrt(P) * math.sqrt(pb))
        y_real = L * (math.sqrt(P) - math.sqrt(pa))

    # 调整显示以匹配代币的小数位数
    x_real_adjusted = x_real / (10 ** decimals_x)
    y_real_adjusted = y_real / (10 ** decimals_y)

    return x_real_adjusted, y_real_adjusted


# 示例参数
L = 171559584868497  # 流动性
sqrt_price_x96 = 25054131037751093162929900172930  # sqrtPriceX96
lower_tick = 111060  # 最小tick值
upper_tick = 122040  # 最大tick值
decimals_x = 9  # 假设Token X有9个小数位
decimals_y = 18  # 假设Token Y有18个小数位

# x:mkndx
# y:eth

# 计算实际储备量
x_real, y_real = calculate_real_reserves(L, sqrt_price_x96, lower_tick, upper_tick)

print(f"x_real整数无精度 = {x_real:.8f}")
print(f"y_real整数无精度 = {y_real:.8f}")
print('<<<==========>>>>')
# 计算实际储备量并调整显示
x_real_dem, y_real_dem = calculate_real_reserves_with_decimals(L, sqrt_price_x96, lower_tick, upper_tick, decimals_x, decimals_y)

print(f"x_real_dem (adjusted) = {x_real_dem:.8f}")
print(f"y_real_dem (adjusted) = {y_real_dem:.8f}")
price内部使用价格:99999.88984058334608562291
adjusted_decimal_price使用精度调整后的价格:0.00009999988984058334
reverted_price(对应UI上的价格10000)(reverted_price=1/adjusted_decimal_price): 10000.01101595380168873817 
x_real整数无精度 = 158385514873.74179077
y_real整数无精度 = 9999999999967028.00000000
<<<==========>>>>
price内部使用价格:99999.88984058334608562291
adjusted_decimal_price使用精度调整后的价格:0.00009999988984058334
reverted_price(对应UI上的价格10000)(reverted_price=1/adjusted_decimal_price): 10000.01101595380168873817 
x_real_dem (adjusted) = 158.38551487
y_real_dem (adjusted) = 0.01000000

计算上下界对应的价格

import math

# Min and Max ticks from the document
min_tick = 111060
max_tick = 122040

# Calculate the prices corresponding to the ticks
price_min = 1.0001 ** min_tick
price_max = 1.0001 ** max_tick

"""
p = y / x; y:eth(18) x:mkdnx(9), 表示y=px,表示x的价格,1个x兑换多少y. // 1个eth兑换多少usdc.
p =
p(调) = y/10^y的精度 / x/10^x的精度 = p * 10^(x精度-y精度) =  p * 10^(9-18)
1/p(调) =
"""

# Adjust for the decimals of USDC and ETH
adjusted_price_min = price_min * (10 ** (9-18))
adjusted_price_max = price_max * (10 ** (9-18))

# Print the adjusted prices in human-readable form
print(f"The minimum price is {adjusted_price_min:.8f} ETH per mkdnx.")
print(f"The maximum price is {adjusted_price_max:.8f} ETH per mkdnx.")
print(f"1/adjusted_price_min: {1/adjusted_price_min:.20f} mkdnx per ETH.")  # 使用 f-string 格式化,保留 20 位小数
print(f"1/adjusted_price_max: {1/adjusted_price_max:.20f} mkdnx per ETH.")  # 使用 f-string 格式化,保留 20 位小数
The minimum price is 0.00006653 ETH per mkdnx.
The maximum price is 0.00019946 ETH per mkdnx.
1/adjusted_price_min: 15030.26478859876260685269 mkdnx per ETH.
1/adjusted_price_max: 5013.43203397946945187869 mkdnx per ETH.

go数据库连接池配置SetConnMaxLifetime和SetMaxOpenConns

代码

	if err != nil {
		log.Fatal("Database connection failed " + err.Error())
	}
	mysqlDB, err := db.DB()
	if err != nil {
		panic(err)
	}

	// 设置空闲模式中的最大连接数 <=0,则不保留空闲连接
	mysqlDB.SetMaxIdleConns(mysqlCfg.MaxIdleConn)

	// 设置与数据库的最大打开连接数 0表示无限制
	mysqlDB.SetMaxOpenConns(mysqlCfg.MaxOpenConn)

	// 设置连接最长时间 d <= 0,连接不会关闭。
	mysqlDB.SetConnMaxLifetime(time.Duration(config.GlobalConfig.Mysql.MaxLifeTime) * time.Minute)

	// 设置连接空闲状态的最长时间 <= 0,连接不会因为连接的空闲时间而关闭。
	mysqlDB.SetConnMaxIdleTime(time.Duration(config.GlobalConfig.Mysql.MaxLifeTime) * time.Minute)

Q1:问题描述

SetConnMaxIdleTime(30 * time.Minute):连接在空闲状态下最长保持时间为 30 分钟。 SetMaxIdleConns(10):每个进程最大空闲连接数为 10。 这2个的关系,如果10个空闲连接,30分钟没有使用,是不是会销毁,然后重建10个新的空闲连接呢?

A:GPT

这两个设置项 SetConnMaxIdleTimeSetMaxIdleConns 共同作用于数据库连接池的管理,但它们的作用是不同的,需要明确理解它们的关系和相互影响。

SetMaxIdleConns

这个配置项指定了连接池中可以保持的最大空闲连接数。即使在没有任何查询请求的情况下,连接池也会保持最多 maxIdleConns 个空闲连接,以便在有新的查询请求时能够快速响应而不需要重新建立连接。

SetConnMaxIdleTime

这个配置项指定了连接在连接池中处于空闲状态的最长时间。如果一个连接空闲超过 maxIdleTime,那么这个连接将被关闭和移除。

两者的关系

  1. 空闲连接的数量管理SetMaxIdleConns 控制的是连接池中允许存在的最大空闲连接数量。例如,如果你设置了 SetMaxIdleConns(10),那么即使在没有任何数据库操作的情况下,连接池也会保持最多 10 个空闲连接。

  2. 空闲连接的时间管理SetConnMaxIdleTime 控制的是每个连接在空闲状态下的最大保持时间。例如,如果你设置了 SetConnMaxIdleTime(30 * time.Minute),那么每个空闲连接如果持续 30 分钟没有被使用,就会被关闭。

具体行为

  • 如果有 10 个空闲连接,并且这些连接在 30 分钟内都没有被使用,那么这些连接会在 30 分钟后被关闭。
  • 如果在连接关闭后有新的查询请求进来,连接池会根据需要重新建立新的连接。

示例解释

假设你设置了以下内容:

mysqlDB.SetMaxIdleConns(10)
mysqlDB.SetConnMaxIdleTime(30 * time.Minute)
  • 当你的应用程序运行时,最多可以有 10 个连接处于空闲状态。
  • 每个空闲连接如果在 30 分钟内没有被使用,就会被关闭。
  • 如果所有 10 个空闲连接都在 30 分钟内没有被使用,那么它们都会被关闭。
  • 随后,如果有新的查询请求进来,连接池会重新建立新的连接,以满足查询请求。

实际应用中的考虑

在实际应用中,这两个参数的配置需要根据你的具体负载和数据库性能来调整。

  • 高负载环境下:可能需要增加 maxIdleConns 以提高响应速度,同时设置一个合理的 maxIdleTime 以防止过期连接占用资源。
  • 低负载环境下:可以适当地降低 maxIdleConnsmaxIdleTime,以减少数据库连接的资源占用。

通过合理配置 SetMaxIdleConnsSetConnMaxIdleTime,你可以确保数据库连接池在性能和资源利用之间达到一个平衡。

Q2:如果没有查询进来,也会保持10个空闲连接吗,还是等连接使用完成后再变成空闲连接,保持30分钟。

A:GPT

在讨论空闲连接的行为之前,需要明确几点:

  1. 空闲连接:空闲连接是指当前没有正在使用,但仍然保留在连接池中的连接。
  2. 最大空闲连接数 (SetMaxIdleConns):这是连接池中允许保持的最大空闲连接数。如果空闲连接多于这个数目,多余的连接将被关闭。
  3. 空闲连接最大保持时间 (SetConnMaxIdleTime):这是一个空闲连接在没有被使用的情况下可以保留的最长时间。超过这个时间,连接将被关闭。

连接池的行为

初始化阶段

当你刚启动你的应用程序时,并没有任何数据库查询请求进来,此时连接池中没有任何空闲连接。SetMaxIdleConns(10) 并不意味着在应用程序启动时就会立即创建 10 个空闲连接。

运行阶段

  1. 当有查询请求进来时
    • 在处理数据库查询的过程中,连接池会根据需要创建新的数据库连接。
    • 一旦查询完成,这些连接会变为空闲连接,并保留在连接池中,前提是连接池中的空闲连接数量小于你设置的 SetMaxIdleConns
  2. 空闲连接的管理
    • 如果连接池中有空闲连接并且这些连接的空闲时间超过了 SetConnMaxIdleTime,这些连接将被关闭。
    • 连接池将始终尝试保持不超过 SetMaxIdleConns 个空闲连接,但不会主动创建空闲连接以达到这个数目。连接池中的空闲连接数是根据实际需求动态调整的。

示例理解

假设你设置了:

mysqlDB.SetMaxIdleConns(10)
mysqlDB.SetConnMaxIdleTime(30 * time.Minute)
  • 应用程序启动时
    • 连接池中没有任何连接(无论是空闲的还是正在使用的)。
  • 当有查询进来时
    • 连接池会创建新的连接来处理这些查询。
    • 查询完成后,这些连接变为空闲连接并保留在连接池中。
  • 空闲连接的管理过程
    • 如果有 10 个空闲连接,并且这些连接在 30 分钟内没有被使用,那么这些连接会在 30 分钟后被关闭。
    • 如果在这段时间内有新的查询进来,连接池会使用这些空闲连接。
    • 如果空闲连接少于 10 个,并且有新的查询进来,连接池会创建新的连接来满足查询需求。

总结

  • 空闲连接数不会主动达到 SetMaxIdleConns:连接池不会在没有任何查询请求的情况下主动创建空闲连接来达到 SetMaxIdleConns 的数量。
  • 空闲连接的生命周期由使用情况决定:只有当有查询请求并使用了连接后,这些连接才能变为空闲连接,并受 SetConnMaxIdleTime 的控制。
  • 空闲连接的关闭:连接池会根据 SetConnMaxIdleTime 的设置,自动关闭超过空闲时间的连接,以确保资源不被长期占用。

这样配置的目的是在高效利用资源和确保查询响应速度之间找到一个平衡点。

Q3:什么情况下会创建新的连接呢,如何知道此时有多少个数据库连接呢?

A:

在数据库连接池管理中,新的连接会在以下几种情况下被创建:

创建新连接的情况

  1. 没有可用的空闲连接
    • 当一个新的查询请求进来时,如果连接池中没有可用的空闲连接,并且当前活跃连接数少于连接池的最大连接数(通常由 SetMaxOpenConns 设置),则会创建一个新的连接来处理这个请求。
  2. 连接池中的可用连接数不足
    • 如果有多个查询请求进来,而连接池中的空闲连接数不足以满足所有请求时,新的连接会被创建,前提是总的活跃连接数没有超过 SetMaxOpenConns

如何知道当前有多少个数据库连接

在 Go 语言中使用 database/sql 包时,标准库本身并不直接提供方法来实时监控当前连接池的状态(比如当前有多少个连接正在使用,有多少个是空闲的)。但是,有几个方法可以间接帮助你了解连接池的状态:

  1. 通过连接池的配置项
    • SetMaxOpenConns:设置最大打开连接数。
    • SetMaxIdleConns:设置最大空闲连接数。
    • SetConnMaxIdleTime:设置连接的最大空闲时间。
  2. 自定义监控和日志
    • 你可以通过中间件或自定义逻辑来记录和监控连接池的使用情况。例如,在每次执行查询、创建连接、关闭连接时,记录日志以监控连接的创建和释放。
  3. 数据库自身的监控工具
    • 许多数据库(如 MySQL、PostgreSQL 等)提供了系统表或视图,可以用来查询当前的连接情况。你可以使用这些工具来实时监控数据库的连接数。

示例:如何监控 MySQL 的连接数

如果你使用的是 MySQL,可以执行以下 SQL 查询来查看当前的连接数:

SHOW STATUS LIKE 'Threads_connected';

这个查询会返回当前连接到 MySQL 服务器的线程数,这实际上代表了当前的连接数。

mysql和mongodb需要强制指定索引的场景

现象

问题原因

问题解决

在使用MySQL和MongoDB时,虽然数据库引擎通常会自动选择最优的索引来执行查询,但有些情况下可能需要强制指定索引,以确保查询性能。这些场景包括但不限于以下几种:

MySQL

  1. 查询优化器选择不理想索引 当MySQL的查询优化器选择的索引并不是最优的,可以使用 FORCE INDEX 强制使用特定的索引。例如:
    SELECT * FROM my_table FORCE INDEX (index_name) WHERE column = 'value';
    
  2. 覆盖索引 当一个索引包含所有需要查询的列时,可以通过指定索引来强制使用覆盖索引,这样可以避免访问表的数据行,提高查询速度。

  3. 避免查询优化器误判 在一些复杂查询中,查询优化器可能会做出不理想的选择,指定索引能避免这种情况。

MongoDB

  1. 复合索引选择 当有多个复合索引存在时,MongoDB查询优化器可能不会总是选择最优的索引。在这种情况下,可以使用 hint 方法来强制指定一个索引。例如:
    db.collection.find({field: value}).hint("index_name");
    
  2. 调试和性能测试 在调试和性能测试时,可以通过 hint 强制使用特定的索引,从而分析不同索引对查询性能的影响。

  3. 避免全表扫描 如果查询优化器误判导致全表扫描,可以通过指定索引来避免这种情况,提高查询性能。

总结

虽然数据库引擎通常能够自动选择最优索引,但在某些特定场景下,强制指定索引是必要的。无论是MySQL还是MongoDB,合理使用索引能显著提升查询性能,但也需要谨慎,避免对数据库性能产生负面影响。

参考资料

  1. 10-MySQL为什么有时候会选错索引?林晓斌

更新Linux上正在运行的程序时, scp更新会报Text file busy

现象

问题原因

问题解决

用mv命令即可

# 备份+覆盖
cp cron cron.back && mv cron /data/bin/cron

参考资料

  1. Text File Busy 问题讨论

  2. linux系统更新正在运行进程的可执行文件需要注意的text file busy的原因及解决方法

为什么基于TCP的应用需要心跳包

Q: 关于TCP保活机制的配置,一共是是这四个配置

是否开启保活。
空闲多久后开始保活探测
多久发送一个词保活探测。
最大探测多少次后可以判断连接空闲,可以关闭。

A:

是的,你总结得非常准确。以下是关于TCP保活的几个配置参数及其作用:

1. 是否开启保活:可以通过设置TCP的keepalive选项来启用或禁用TCP保活机制。当启用TCP保活时,操作系统会在连接空闲时发送保活探测来检测连接的有效性。

2. 空闲多久后开始保活探测:通过TCP_KEEPIDLE参数来设置,表示连接在空闲多少秒后开始发送保活探测。如果在这段时间内没有数据传输或接收,操作系统会发送保活探测来检测连接的有效性。

3. 多久发送一个保活探测:通过TCP_KEEPINTVL参数来设置,表示两次保活探测之间的间隔时间。如果在空闲时间超过TCP_KEEPIDLE设置的阈值后,每隔TCP_KEEPINTVL秒,操作系统会发送一次保活探测。

4. 最大探测多少次后可以判断连接空闲,可以关闭:通过TCP_KEEPCNT参数来设置,表示在没有从对端获得响应的情况下,发送保活探测的最大次数。如果在达到最大次数后仍然没有获得对端的响应,操作系统将判定连接为空闲,可以关闭这个连接。

通过调整这些参数,可以根据实际需求来定制TCP保活机制的行为,以提高连接的可靠性和资源利用效率。****

参考

1.为什么基于TCP的应用需要心跳包(TCP keep-alive原理分析)

抓包可以看到两类情况

  • 正常探活: 每20秒发送探测包(keep-alive包,tcp_keepalive_time配置为20时)

  • 网络连接出问题了: 20秒发送探测包没有收到回复后,判断连接出问题了,继续探测8次,也就是每隔一秒重发了9次tcp keep-alive,9次都没有响应,则发送rst包关闭连接。

pg什么时候加载shared_buffers

Q: 什么时候加载shared_buffers?

A: 1.在访问数据时,数据会先加载到os缓存,然后再加载到shared_buffers,这个加载过程可能是一些查询,也可以使用pg_prewarm预热缓存。

A: PostgreSQL缓存读顺序share_buffers -> 操作系统缓存 -> 硬盘。

那么也可能是操作系统缓存不足,而不定是share_buffers。

通过文章中vmstat命令看到cache有260G,free值也很稳定,所以应该检查PostgreSQL的缓存。(老师执行vmstat是不是埋了个伏笔)。

参考

1.32丨当Postgres磁盘读引起I/O高的时候,应该怎么办?

2.什么时候加载shared_buffers

CPU使用率低负载高

使用场景

我发现我本地磁盘使用了300多g,想清理一下,最开始使用du -h --threshold=1G -d 1 /path/to/directory来查看指定目录下超过1GB的文件和子目录的磁盘占用情况 但是该命令导致机器的负载从5,变为25,电脑很卡,所以改为使用以下go代码进行扫描,结果跑几十秒后负载也上去了。

package main

import (
	"fmt"
	"os"
	"path/filepath"
)

func main() {
	root := "/"
	err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			fmt.Printf("Error accessing path %q: %v\n", path, err)
			return nil
		}
		if info.Size() > 1<<30 { // 判断是否超过1GB
			fmt.Printf("%q: %d bytes\n", path, info.Size())
		}
		return nil
	})
	if err != nil {
		fmt.Printf("Error walking the path %q: %v\n", root, err)
	}
}

我的电脑配置:ubuntu 22,16核,32g,htop查看到负载很高时,cpu使用率不超过50%,内存也没啥变化,主要就是软件占用的内存,差不多10g,也没啥变化。

大概搜索了一下,负载和cpu使用率没有很大联系,负载表示的是平均活跃进程数,只能说明最近几分钟有活跃的进程,并且这些进程不怎么占用cpu,而是占用的io,或者等待io,或者一直在竞争cpu但是没有使用。

涉及的知识点:

1.处于r和d状态的进程会算入负载的的进程数。

进一步排查的方法:

1.iotop 查看io占用大的进程.

2.iostat 查看磁盘负载iostat

参考

1.记一次CPU使用率低负载高的排查过程

tips:系统load高,不代表cpu资源不足。Load高只是代表需要运行的队列累计过多。但队列中的任务实际可能是耗cpu的,也可能是耗i/0及其他因素的

但是system的中断数(in)、上下文切换(cs)特别频繁,进程上下文切换次数较多的情况下,很容易导致CPU将大量的时间耗费在寄存器、内核栈、以及虚拟内存等资源的保存和恢复上,进而缩短了真正运行进程的时间造成load高。

  • 排查单个进程的具体线程的上下文切换情况

通过vmstat只能查看总的cpu上下文切换,可通过pidstat命令查看线程层面的上下文切换信息 pidstat -wt 1 (下图拉的是9s的数据,总共36w次,平均每秒4w次)

cswch/s:

nvcswch/s: TODO 比如IO等待让出???

2.【Linux负载系列-2】Linux CPU 使用率低 Load 负载高场景测试

3.load高但是cpu占用率低的排查

4.top命令输出解释以及load average 详解及排查思路

5.进程上下文频繁切换导致load average过高

  • 1、LMbench 是带宽(读取缓存文件、内存拷贝、读写内存、管道等)和反应时间(上下文切换、网路、进程创建等)的评测工具;

  • 2、micro-benchmark contextswitch 可以测试不同的CPU在最少多少ns可以进行一次上下文件切换,再转化为秒,我们可以确认该处理器每可以进行的上下文件切换数 ,该工具的使用可以参看tsuna的blog。

cswch/s: 每秒任务主动(自愿的)切换上下文的次数,当某一任务处于阻塞等待时,将主动让出自己的CPU资源。

nvcswch/s: 每秒任务被动(不自愿的)切换上下文的次数,CPU分配给某一任务的时间片已经用完,因此将强迫该进程让出CPU的执行权。

6.【Linux负载系列-1】Linux 系统的平均负载(Load Avarage)

内核中计算平均负载的源码

long calc_load_fold_active(struct rq *this_rq, long adjust)
{
	long nr_active, delta = 0;

	nr_active = this_rq->nr_running - adjust;
	nr_active += (long)this_rq->nr_uninterruptible;

	if (nr_active != this_rq->calc_load_active) {
		delta = nr_active - this_rq->calc_load_active;
		this_rq->calc_load_active = nr_active;
	}

	return delta;
}
这段代码来自于 Linux 内核的 loadavg.c 文件,它负责计算系统的平均负载。具体来说,平均负载是衡量系统活动的指标之一,表示系统在一段时间内运行的进程数量和等待CPU资源的进程数量的平均值。

代码解析如下:

calc_load_fold_active 函数用于计算每个 CPU 的活跃进程数,并将其变化量(delta)返回。活跃进程数是指正在运行的进程(nr_running)和不可中断状态的进程(nr_uninterruptible)的和。

nr_active 通过从当前 CPU 的正在运行的进程数中减去一个调整值(adjust),然后加上不可中断状态的进程数来计算。这种不可中断状态一般表示进程正在等待某些类型的 I/O 操作完成。

如果新计算的活跃进程数与最后一次记录的值(保存在 this_rq->calc_load_active)不同,代码将计算两者之间的差值(delta),更新 calc_load_active,并返回这个差值。

在这段代码中,注释提到了分布式计算负载的方式。由于在多 CPU 系统中,遍历所有 CPU 来计算平均负载可能会非常耗时,所以 Linux 采取分布式和异步的方式来最小化计算开销。每个 CPU 计算自己的活动进程数的变化量,并将其贡献到全局平均负载的计算中。

此外,注释还提到了几个与 NO_HZ(即时钟节能模式)相关的复杂性,以及 nr_uninterruptible 的统计可能并不是精确的,因为为了避免增加额外的跨 CPU 缓存行访问和原子操作,它可能在不同 CPU 之间被统计。

总体来看,这段代码是内核计算平均负载的关键部分,它使用了一些性能优化的技巧来处理在具有大量 CPU 的系统上计算负载的问题。
Q: (long)this_rq->nr_uninterruptible; 这个状态的进程一般是什么情况

A:
在 Linux 操作系统中,进程状态 `(long)this_rq->nr_uninterruptible` 表示进程处于不可中断睡眠状态(D状态),这通常是因为进程正在等待某种不可忽略的事件完成,通常是 I/O 操作。在这种状态下,进程不会响应异步信号。这个设计的目的是为了保护进程在关键操作期间不被打断,从而避免数据的不一致或者损坏。

不可中断睡眠状态的进程通常涉及到下面这些情况:

1. **磁盘 I/O**:进程正在等待磁盘读写操作完成,例如从硬盘读取数据或向硬盘写入数据。

2. **网络 I/O**:进程可能在等待网络操作完成,如等待网络数据包的到来。

3. **设备 I/O**:如果进程正在与其他类型的设备通信(比如 USB 设备或者其他外围设备),它可能需要等待设备的响应。

4. **等待硬件条件**:进程可能在等待一些特定的硬件条件,比如等待某个硬件中断。

在这个状态下,进程将被排除在调度器可运行进程队列之外,并且不会被分配CPU时间,直到它等待的事件被处理。一旦 I/O 或其他等待事件完成,进程状态会转变,使其能够继续执行。

在 `calc_load_fold_active` 函数内部,这个状态的进程数量被统计并包含在系统负载的计算中,因为这些进程虽然当前不占用 CPU 资源,但它们表示了系统中未完成的工作量。由于这些进程一旦 I/O 完成就可能立即变为可运行状态,它们对系统负载的影响是有意义的。

golang模版引擎

使用场景

  1. cms系统的自定义标签: 自定义网站标签,渲染网站模版

例子

package main

import (
	"net/http"
	"text/template"
)

type DataForTpl struct {
	Id   int
	Name string
}

// 模板自定义函数及管道  https://www.lesscode.work/sections/62ac21319741b.html
func main() {
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		// 定义函数
		// say := template.FuncMap{"say": func(message string) string {
		// 	return "say x : " + message
		// }}
		// say2 := template.FuncMap{"say2": func(data DataForTpl) string {
		// 	return "say2 y: " + data.Name
		// }}
		say := template.FuncMap{
			"say": func(message string) string {
				return "say x : " + message
			},
			"say2": func(data DataForTpl) string {
				return "say2 y: " + data.Name
			}}
		t := template.New("index.html")
		t.Funcs(say)
		// t.Funcs(say2)
		// t.ParseFiles()
		_, _ = t.Parse(`<!DOCTYPE html>
<html lang="en">
<head>
</head>
<body>
<!--理解函数的对象需要和传入的参数匹配-->
<div>hi,我是文本模版...</div>
<div>hi...</div>
</body>
</html>`)
		// 传入数据
		err := t.Execute(w, DataForTpl{
			Id:   431,
			Name: "test..",
		})
		if err != nil {
			println(err.Error())
		}
	})
	http.ListenAndServe(":8088", nil)
}

技术点

1.模版渲染过程 parse和execute

1.1 parse

就是把待解析的文本,解析为树的形式,并且拆分为几个节点. 这里的例子,把文本拆分为了3个文本节点(textNode)和2个执行节点(actionNode).

_config.yml

  • GPT问答
Q1:文本变成解析树,需要哪些知识点

A:将文本解析成解析树涉及以下几个知识点:

1. 词法分析(Lexical Analysis):词法分析是将输入的文本字符串分解成一系列标记(tokens)的过程。在模板解析中,词法分析器会扫描文本并识别出模板语言中的各种标记,如变量、函数、控制结构等。

2. 语法分析(Syntax Analysis):语法分析是将词法分析得到的标记序列转换为抽象语法树(Abstract Syntax Tree,AST)的过程。在模板解析中,语法分析器会根据模板语言的语法规则,构建出表示模板结构的语法树。

3. 语法规则(Syntax Rules):模板语言具有特定的语法规则,定义了如何组织模板中的标记和表达式,以及它们之间的关系。了解模板语言的语法规则是理解和实现模板解析的关键。

4. 解析器(Parser):解析器是实现语法分析的程序或算法。它接受词法分析器生成的标记序列,并按照语法规则构建语法树。解析器可以使用递归下降、LL(k)、LR(k) 等不同的算法来进行解析。

5. 解析树(Parse Tree):解析树是由语法分析器构建的树形结构,它反映了模板文本中标记和表达式之间的层次关系。解析树可以用于后续的模板渲染和执行过程。

理解以上知识点可以帮助你理解模板解析的工作原理,并在需要时实现自己的模板解析器。在 Go 语言中,`text/template` 包已经提供了完整的模板解析和执行功能,可以直接使用其中的解析器来将文本解析成解析树。


Q2:上面的parse就经过了这几个步骤,是吗

A:
是的,上述的 `parse` 是 `text/template` 包中的一个类型,它代表了模板的解析树。在 Go 语言中,使用 `text/template` 包来进行模板解析时,以下步骤会被自动完成:

1. 词法分析:`text/template` 包内部包含了词法分析器,它会将输入的模板文本进行词法分析,将文本分解成一系列的标记(token)。

2. 语法分析:`text/template` 包内部的解析器会根据模板语言的语法规则,接收词法分析器生成的标记序列,并构建出表示模板结构的解析树。

3. 构建解析树:解析器会按照语法规则逐步解析标记序列,并构建出解析树的节点和层次结构。解析树的节点表示模板中的标记、表达式和控制结构等。

4. 解析错误处理:如果在解析过程中遇到语法错误,解析器会报告错误并提供相应的错误信息,以帮助开发者修复模板中的问题。

最终,`parse` 对象会保存整个模板的解析结果,包括解析树和相关的元数据。通过解析树,可以在后续的模板渲染过程中进行变量替换、执行控制结构等操作,从而生成最终的输出文本。


Q3: 那接下来的execute,又是怎么把数据渲染到解析树中的呢


A:在 `text/template` 包中,`Execute` 函数用于将数据渲染到解析树中生成最终的输出文本。执行过程如下:

1. 创建模板对象:首先,你需要创建一个模板对象。可以使用 `template.New` 或 `template.Must` 函数来创建一个模板对象。例如:`tmpl := template.Must(template.New("myTemplate").Parse(templateText))`。

2. 准备数据:将要渲染到模板中的数据准备好,并将其传递给 `Execute` 函数。数据可以是任意类型的值,可以是单个值,也可以是结构体、映射等复杂类型。

3. 执行渲染:调用模板对象的 `Execute` 方法,将数据作为参数传递进去。例如:`err := tmpl.Execute(writer, data)`。其中,`writer` 是一个实现了 `io.Writer` 接口的对象,用于接收渲染后的输出结果。

4. 渲染过程:`Execute` 函数会按照解析树中定义的规则,遍历解析树的节点,并根据节点的类型执行相应的操作。对于变量节点,会从数据中检索对应的值;对于控制结构节点(如循环、条件语句等),会根据条件判断是否执行相应的代码块。

5. 输出结果:渲染过程会将最终的输出结果写入到 `writer` 中,可以通过该对象获取渲染后的输出文本。

通过以上步骤,模板中的标记和表达式会被替换为数据中的对应值,控制结构会根据条件进行相应的执行,最终生成渲染后的输出文本。

需要注意的是,在实际使用过程中,还可以使用管道操作符 `|` 和自定义函数来对数据进行加工处理,以满足更复杂的渲染需求。同时,还可以使用模板语言中的控制结构、循环语句等功能来实现动态的输出文本。

1.2 execute

就是把数据填充到模版树的节点中去

2.组合嵌套模版

参考

1.golang的一些ssti模板注入问题

postgresql三表查询性能优化的例子

TODO

go语言如何最大化利用cpu时间?&&Go为什么这么快?

1.Go调度器经过几个大版本的迭代才有今天的优异性能

// 附录2

Q:4. go语言如何(在windows, linux)最大化利用 cpu 时间? (讲出实现原理)

// 附录1

Go 程序提供了网络轮询器(NetPoller)来处理网络请求和 IO 操作的问题,其后台通过 kqueue(MacOS),epoll(Linux)或 iocp(Windows)来实现 IO 多路复用。

A: 1.对于系统调用,用了epoll,让cpu最大化的有用户太线程干活儿

2.对于非系统调用,一直有goroutine在运行,内核线程不会停

本文主要从 Go 调度器架构层面上介绍了 G-P-M 模型,通过该模型怎样实现少量内核线程支撑大量 Goroutine 的并发运行。

以及通过 NetPoller、sysmon 等帮助 Go 程序减少线程阻塞,充分利用已有的计算资源,从而最大限度提高 Go 程序的运行效率。

参考

1.Go 为什么这么快 GPM模型简介 2. 3.可视化GMP分析trace(TODO)

DOCKER默认虚拟网卡IP地址段导致的网络访问异常问题(线上事故)

现象

php的前端服务,部署在k8s中,多个pod,调用部署在阿里云物理机器的http服务, 偶尔差不多20%的请求会很慢(耗时30s左右)。

调用链路:

php(k8s pod) -> 阿里云SLB(http负载均衡) -> 阿里云ECS(2台物理机器)http服务

1.注意2台物理机器里面装了docker,产生了虚拟网卡,且虚拟网卡的网段和k8s的某些机器的网段重了。 这个docker并没有启动任务服务,只是安装docker,且产生了虚拟网卡.

2.pod可以ping通物理机,但是无法telnet通物理机。

问题原因

简单说就是,www在k8s里面,请求阿里云物理机器的推荐服务,推荐服务的物理机装了docker产生虚拟网卡和k8s的pod的地址重了。 导致www的请求过来在 推荐服务的机器上,一直回环绕,直到底层的tcp链接超时了,才返回报错。

问题解决

1.修改docker虚拟网卡的ip。 2.非必要不安装docker. 3.尽量迁移k8s部署。

参考资料

1.DOCKER默认虚拟网卡IP地址段导致的网络访问异常问题 2.docker网段冲突导致主机网络异常处理

mysql索引失效-IN语句参数过多(线上事故)

case IN语句包含过多的id时,mysql优化器估算走索引比全表扫描慢,从而全表扫描

// 附录4

  1. IN底层执行是通过多次执行range范围查询来处理所有IN参数的。随着IN参数过多,mysql优化器估计走索引不比全表扫描成本低,所以走全表扫描.

_config.yml

  1. 系统变量eq_range_index_dive_limit对IN子句的影响

_config.yml

The optimizer will use existing index statistics instead of doing index dives for equality ranges if the number of equality ranges for the index is larger than or equal to this number. If set to 0, index dives are always used.

参考资料

1.我们都是小青蛙MySQL系列文章.pdf–包含IN语句的执行过程 2.TODO MySQL not using indexes with WHERE IN clause?–IN不走索引比较细致的分析 2.1 3.MySQL索引原理及慢查询优化 4.终极版本:美团开发的基于sql自动创建索引的系统方案–基于代价的慢查询优化建议 https://juejin.cn/post/6971387163799715876

mysql索引失效

相关搜索资料

https://www.google.com/search?q=mysql%EF%BC%8C%E4%BB%80%E4%B9%88%E6%97%B6%E5%80%99%E7%B4%A2%E5%BC%95%E5%A4%B1%E6%95%88&oq=mysql%EF%BC%8C%E4%BB%80%E4%B9%88%E6%97%B6%E5%80%99%E7%B4%A2%E5%BC%95%E5%A4%B1%E6%95%88&aqs=chrome..69i57.546j0j7&sourceid=chrome&ie=UTF-8

1.原理

2.How do MySQL indexes work?

3.实战

4.我们都是小青蛙MySQL系列文章.pdf–包含IN语句的执行过程

控制协程(goroutine)的并发数量

背景

附录1

1.对单个 file/socket 的并发操作个数超过了系统上限,这个报错是 fmt.Printf 函数引起

2.文件句柄数是有限的

3.内存是有限的

# 1.控制协程(goroutine)的并发数量

es奔溃和优化

背景

#

  • 1.查看old分区的gc耗时

从平时的不到0.5ms变为3ms,扩大6倍

_config.yml

  • 2.查看old分区使用情况

old分区应该是2g。

差不多30分钟-90分钟就会从900MB达到1.8G触发old gc

注意到,Old区的内存持续增长,30-90分钟内存就会将Old区填满,经过Old GC几乎全部可以回收掉一半。

  • 3.old区gc频次

每次在old区快用完时,gc频次监控都有个尖刺。

_config.yml

  • 4.注意到同时也有full gc

  • _config.yml

猜测

1.es大量写入,refresh和fsync操作比较费资源,导致es负载增加

参考

1.Elasticsearch GC 优化实战 2.es写入原理

golang本地缓存选型对比及原理总结

背景

最近做搜索推荐,线上预测服务需要读取大量的特征,对内存缓存的需求比较,所以调研了一下go内存缓存相关库.

主要关注点

1.是否零gc, 无gc原理

2.expire原理

3.数据淘汰机制

4.优势,劣势

附录

1.golang本地缓存选型对比及原理总结

某一台容器的 IO 很高,就会导致同主机上的其他容器服务延时增加

背景

最近公司把服务部署都迁移到k8s上了,我对docker和k8s进行了一定的复习。

突然想到很久之前记录的关于 docker无法隔离io 的经历。 特此记录到博客中,以免未来忘了这事。

结论

原来docker不能隔离io,是因为所有容器都共享了bufferdio的原因。

我理解是如果一个高io容器占据了全部的 Bufferdio,那么肯定会导致其他容器无法使用Bufferdio。

这个问题是我在成都的公司遇到的,就是我们的容器无法隔离io,只要有一台容器频繁的访问磁盘,就会导致其他容器变慢,现在看来确实是这样。

美团通过修改内核,可以给每个容器单独配置bufferdio

_config.yml

美团扩展k8s增加了更多的限制,同时还修改了core dump的流程

  1. 除了常见的 CPU、内存外,还有 IO 的限制、ulimit 限制、PID 限制等等。所以我们扩展了 K8s 来完成这些工作。

  2. 对 core dump 的流程进行了修改,让 dump 文件写到容器自身的文件系统中,并且使用容器自己的 Cgroup IO 吞吐限制。

附录

1.美团容器平台架构及容器技术实践

【转载】Mac M1 通过docker部署Centos7、MySql等常用镜像

# 1.Mac M1 通过docker部署Centos7、MySql等常用镜像

快速生成curd的Web管理平台(golang+vue)

背景

一般业务开发会经常有对某个进行crud的诉求,这个其实是可以做配置来提供增删改查的。

我之前公司的后台管理平台是可以做到UI配置化来支持对某个表的crud的。

golang这边找到一个开源的代码,支持配置文件:takoyaki

docker logs

docker logs原理

docker logs 命令会吧容器内的stdout、stderr 标准输出和标准错误都显示出来。具体原理见下文

使用

# 查看qp容器的最新日志
docker logs --tail 10 qp -f

# 1.docker logs 2.Docker笔记(十三):容器日志采集实践

mysql性能优化:Query Profiler和Explain

#

第39周   数据库监控与调优
本周我们来一起解决数据库监控与调优的问题,通过本周的学习,基本上可以搞定慢查询,索引等数据库重难点性能调优问题。
课程安排:
1、数据库监控 - Prometheus
2、慢查询日志
3、索引分析与优化
4、锁优化,Query Profiler
5、SQL优化工具
6、MySQL配置优化
7、拓展分享:如何在合适的场景用合适的数据库?
8、拓展分享:业务妥协会对系统调优造成怎样的影响?

1.Query Profiler 和Explain 用法详解 2.Java架构师-技术专家

select中既有聚合字段,又有非聚合字段

背景

SELECT
	t1.page_role,
	t2.page_id,
	t2.config,
	t2.style 
FROM
	t_page t1
	LEFT JOIN t_page_new_content t2 ON t1.id = t2.page_id 
WHERE
	t1.can_recommend = 1 
	AND t2.page_id IN (10758,10757,10761)
	AND t2.style IN ( 'img_tag_text', 'billboard', 'head_img_list' ) 
	AND ( ( start_time <= NOW( ) AND end_time > NOW( ) ) OR permanent = 1 ) 
	GROUP BY t2.page_id
	LIMIT 3

t2.page_id 是主键或者唯一键时,这个语句就是合法的,不管sql_mod是不是 ONLY_FULL_GROUP_BY

阿里云配置

_config.yml

_config.yml

参考

1.关于MySQL中only_full_group_by模式的一点个人理解 2.The query is valid if name is a primary key of t or is a unique NOT NULL column

golang标准库http的client为什么必须手动关闭resp.Body

背景

对为什么要执行下面的语句有些忘了,这里记录一下。

defer resp.Body.Close()

参考

golang标准库http的client为什么必须手动关闭resp.Body

update导致undolog膨胀,mysql磁盘占用膨胀 && kill不掉的语句

背景

update t_user_voucher set seller_support=0 where id in(select id from tmp_muvids_0311) limit 1;

最近同事在做重构,涉及数据迁移和数据修复,线上执行了这个语句。

阿里云就收到了报警:

云数据库RDS版 发生告警
时间: 15:13
实例名称: 线上mysql_8
实例详情: xxxx
监控指标: 磁盘使用率
报警条件: (80.99>80)
持续时间: 8小时4分钟

原因定位过程

  1. 最开始是以为有什么上线,导致数据增长比较多。所以问了下3个业务组最近有哪些上线。结果没有上上线。

  2. 因为我们线上是一主三从,然后一个同事发现, 只有主库报警,从库没有报警,主库磁盘占用 比 从库磁盘占用 多了60G。

  3. 然后阿里云提了工单,给我们看了mysql8数据库的数据库文件相关的信息

// 我做了一定处理
库	269G	no	
.库/表.ibd	14G
.库/表.ibd	13G

undo log存放的位置是

A: 好像是在.idb文件中??? TODO

// 附录6

InnoDB默认是将Undo-log存储在xx.ibdata共享表数据文件当中,默认采用段的形式存储

show variables like 'innodb_file_per_table';
说明线上为独占表空间

同时发现
show variables like 'innodb_undo_tablespaces';

然后参数innodb_max_undo_log_size为1073741824。

对于事务回滚原理的纠正

// 附录6

实际上当一个事务需要回滚时,本质上并不会以执行反SQL的模式还原数据, 而是直接将roll_ptr回滚指针指向的Undo记录,从xx.ibdata共享表数据文件中拷贝到xx.ibd表数据文件,覆盖掉原本改动过的数据。

如何处理巨大的回滚段? TODO

// 附录7

没啥好办法,大数据量,回滚就是很耗时。

可以考虑分批提交。

我们执行kill语句,发现不能kill。

// 参考附录3

_config.yml

参考

1.03 | 事务隔离:为什么你改了我还看不见?

1.1 回滚日志总不能一直保留吧,什么时候删除呢?

1.2 回滚段被清理,文件也不会变小? TODO 这是为啥

1.3 基于上面的说明,我们来讨论一下为什么建议你尽量不要使用长事务。 长事务意味着系统里面会存在很老的事务视图。由于这些事务随时可能访问数据库里面的任何数据,所以这个事务提交之前,数据库里面它可能用到的回滚记录都必须保留,这就会导致大量占用存储空间。

2.参考我之前博客

3.MySQL UNDO表空间独立和截断

4.万答#18,MySQL8.0 如何快速回收膨胀的UNDO表空间

5.32 | 为什么还有kill不掉的语句? 所以,如果你发现一个线程处于 Killed 状态,你可以做的事情就是,通过影响系统环境,让这个 Killed 状态尽快结束。

6.InnoDB默认是将Undo-log存储在xx.ibdata共享表数据文件当中,默认采用段的形式存储

7.MySQL的rollback–大事务回滚

MySQL中的kill命令,你用过吗?

背景

在MySQL中,kill命令分为如下两种:

1、kill query + pid

2、kill connection + pid 其中connection可以省略

information_schema.INNODB_TRX列解析

// 附录2

SELECT * FROM information_schema.INNODB_TRX\G

kill后回滚整个过程解析

// 附录2 查看innodb的事务表,发现刚才kill的语句正在进行回滚操作。 trx_rows_modified 代表锁影响的行数,当数值为0时,锁将会释放。

mysql> SELECT * FROM information_schema.INNODB_TRX\G
*************************** 1. row ***************************
                    trx_id: 71735
                 trx_state: ROLLING BACK
               trx_started: 2021-06-03 14:17:40
     trx_requested_lock_id: NULL
          trx_wait_started: NULL
                trx_weight: 462200354
       trx_mysql_thread_id: 8
                 trx_query: create table fact_sale_new as select * from fact_sale
       trx_operation_state: rollback of SQL statement
       ///////////

参考

1.MySQL中的kill命令,你用过吗? 2.MySQL kill进程后出现killed information_schema.INNODB_TRX列解析 kill后回滚整个过程解析

3.

入门使用ANTLR词法语法分析工具

背景

参考

1.入门使用ANTLR词法语法分析工具–B站开源了golang版的规则引擎, 可以动态加载DSL程序代码

Go Data Structures,Graph

背景

最近公司有战略合作,得到了某中型电商公司的技术支援.提供了部分搜索系统源码(java)。 其中设计图执行引擎相关的东西,所以我用go试一试图数据结构的实现。

参考

1.Go Data Structures: Graph 2.Graph algorithms and data structures

IK分词器优化

背景

最近在做搜索架构重构,在调研如何自己实现分词服务的方法。 需求是可以保持ik主要的分词逻辑,同时又能定制一些分词逻辑。

实现思路:

找ik源码改下(java语言),额外独立一个分词服务,在服务内部做代码调整,这样方便改动和部署上线。

ik分词器原理和debug

附录1

ik分词器总结

// 附录1 总的来说

  1. IK分词是一个基于词典的分词器,只有包含在词典的词才能被正确切分,
  2. IK解决分词歧义只是根据几条可能是最佳的分词实践规则,并没有用到任何概率模型,
  3. 也不具有新词发现的功能。

参考

1.IK分词器 原理分析 源码解析 debug-ik分词器

2.IK分词器优化

Redis常见集群方案(Codis和Redis-cluster对比)

背景

最近在做搜索推荐系统的架构重构,参考了转转公司的架构方案。 现阶段正在进行一些技术和存储的选型。

这篇文章记录缓存存储的选型。

codis是什么

Redis集群方案目前主流的有三种,分别是Twemproxy、Codis和Redis Cluster。

内容

附录1讲述了高可用redis集群方案的演进过程

参考

1.Codis和Redis-cluster对比

转载-探讨两种 option 编程模式的实现

背景

go-micro源码中和我们的线上代码大量用到了option模式。 发现一篇博客总结得全面,这里记录一下读后感。

参考

1.探讨两种 option 编程模式的实现 2.GO 编程模式:FUNCTIONAL OPTIONS

关于code review需要进行哪些内容

背景 TODO

关于code review需要进行哪些内容

参考

1.Uber Go 语言编码规范

pprof排查思路梳理-TODO

背景

最近团队项目增多,频繁遇到内存泄漏,携程泄漏等问题。我们对pprof使用思路不是很清晰。这里梳理一下

什么场景应该用什么pprof?

附录1

1.内存泄漏

go tool pprof http://localhost:6060/debug/pprof/heap

2.携程泄漏

go tool pprof http://localhost:6060/debug/pprof/goroutine

3.gc问题(一般问题不大)

所以接下来使用 pprof 排查时,我们在乎的不是什么地方在占用大量内存,而是什么地方在不停地申请内存,这两者是有区别的。

# 为了获取程序运行过程中 GC 日志我们在启动前赋予一个环境变量同时为了避免其他日志的干扰使用 grep 筛选出 GC 日志查看
GODEBUG=gctrace=1 ./go-pprof-practice | grep gc


go tool pprof http://localhost:6060/debug/pprof/allocs

heap的采样原理

// 附录2 heap: 包含每个 goroutine 分配大小,分配堆栈等。每分配 runtime.MemProfileRate(默认为512K) 个字节进行一次数据采样。

实践 Tips

附录2

打印当前系统内存使用情况

func showMemoryUsage() {
    Runtime runtime = Runtime.getRuntime();
    long free = runtime.freeMemory();
    long total = runtime.totalMemory();
    long used = total - free;

    long totalMB = total / (1024 * 1024);
    long usedMB = used / (1024 * 1024);
    LOG.debug("Memory usage: " + usedMB + " of " + totalMB + "M");
}

如何阅读pprof生成的图形

附录3,4

FAQ:分配堆内存比较多的地方并不一定产生内存泄漏,比较2个时间点分配内存的差值,容易就是内存泄漏

现在你就可以比较这两个时间点的堆的差异了: go tool pprof –base base.heap current.heap 操作和正常的go tool pprof操作一样, 比如使用top查看使用堆内存最多的几处地方的内存增删情况

但是, 分配堆内存比较多的地方并不一定产生内存泄漏,只能说明这个地方”曾经/正在”分配的堆内存比较大,或者分配的堆内存比较频繁俄安,这些分配的内存可能在之后就回收掉了。

像Java的一些profiler工具一样, pprof也可以比较两个时间点的分配的内存的差值,通过比较差值,就容易看到哪些地方产生的内存”残留”的比较多,没有被内存释放,极有可能是内存泄漏的点。

使用多年的go pprof检查内存泄漏的方法居然是错的—对比2个heap的对象变化

demo:2022-06-23

最近有个线上推荐服务,是读取1个1-2的模型文件,进行预测,然后提供对多路召回的商品进行实时排序。

性能现象:

1.qps<10,单个接口<100ms。

2.qps>10,单个接口马上400-500ms。

3.内存使用率50%, cpu使用率60-70%, 所以不是资源不足的问题。

  • 分析方法 2个同事分开分析

我:pprof,cpu,heap + 打印各个部分耗时。

另一个同事:查看源代码,主要关注耗时较多环节的代码。

  • 主要操作

1.sql耗时,改为缓存

2.因为不是资源问题,猜测锁等待或gc,导致接口响应慢。

pprof cpu查看耗时最多的函数,然后查看代码。

3.逐步注释代码,查看压测结果,只要注释部分代码,性能就很好,所以定位到部分问题代码。

解析FFM模型的特征组合部分的参数时,直接存储为:

k: feat_id和field组合,通过"_"拼接
 v: (特征id和特征域)组合的权重向量数组
  Fea_id_field_map map[string][K_vector]float64


导致使用参数打分时,需要使用`fmt.Sprintf("%d_%d", fea_id_i, fea_id_field_map[fea_id_j]`
构造key,导致产生大量字符串同时pprof查看到fmt.Sprintf的耗时居然占了整个cpu火焰图的40%,所以想办法去掉整个字符串拼接

vi_fj_vector := components.Global_Ffm_model.Fea_id_field_map[fmt.Sprintf("%d_%d", fea_id_i, fea_id_field_map[fea_id_j])]

4.调整模型数据结构。消除字符串拼接

  1. go1.16升级go1.18,但是看监控gc更慢了(从0.5ms到2ms的gc耗时),不过整体性能更好了。也可能是上面的原因。
  • 成果 1.qps 70, p99<100ms, 满足技术部线上性能要求。

参考

1.golang pprof 实战

2.go pprof 性能分析-pprof 数据采样

3.Go高性能系列教程:读懂pprof生成的报告

4.Go常用包(三十):性能调试利器使用(中)

5.实用go pprof使用指南

flat flat%
一个函数内的directly操作的物理耗时。例如

func foo(){
    a()                                        // step1
    largeArray := [math.MaxInt64]int64{}       // step2
    for i := 0; i < math.MaxInt64; i++ {       // step3
            c()                                    // step4
    }
}
flat只会记录step2和step3的时间;flat%即是flat/总运行时间。内存等参数同理。
cum cum%
相比flat,cum则是这个函数内所有操作的物理耗时,比如包括了上述的step1、2、3、4。

一般cum是我们次关注的,且需要结合flat来看。flat可以让我们知道哪个函数耗时多,而cum可以帮助我们找到是哪些函数调用了这些耗时的(flat值大的)函数。
火焰图的横向长度表示cum,相比下面超出的一截代表flat。// 也就是某个函数本身直接执行的耗时,不包括其子流程的耗时

实战Go内存泄露

背景

内部项目(notify-srv-codeup)存在携程泄漏 4188ece1 jzw xxxx@qq.com on 2022/2/18 at 2:07 下午 从消息队列获取消息,每个消息产生一个携程,go监控的当前携程数量和消息发送数量相当。原因比较明细,但是该项目携程嵌套使用层次比较深。具体定位不到哪一行。

#

参考

5.实战Go内存泄露

docker容器占用磁盘大,迁移数据目录 TODO

本文回答2个问题

1.docker容器占用磁盘大,如何做数据迁移。TODO 应该是迁移容器层的东西?

2.OverlayFS 是怎么工作

背景

测试环境用docker运行了很多容器。且几年前安装时默认使用的系统盘存储。 想迁移到数据盘中。

Q: 1.docker容器占用磁盘大,如何做数据迁移。

附录3,4 分别对应软链接和移动全部数据的方式

Q:2.OverlayFS 是怎么工作

// 附录1,2 OverlayFS 也是把多个目录合并挂载,被挂载的目录分为两大类:lowerdir 和 upperdir。

lowerdir 允许有多个目录,在被挂载后,这些目录里的文件都是不会被修改或者删除的,也就是只读的;

upperdir 只有一个,不过这个目录是可读写的,挂载点目录中的所有文件修改都会在 upperdir 中反映出来。

容器的镜像文件中各层正好作为 OverlayFS 的 lowerdir 的目录,然后加上一个空的 upperdir 一起挂载好后,就组成了容器的文件系统。

参考

1.OverlayFS 也是把多个目录合并挂载,被挂载的目录分为两大类:lowerdir 和 upperdir

2.[07 白话容器基础(三):深入理解容器镜像](https://time.geekbang.org/column/article/17921?cid=100015201)

3.最方便的docker数据目录迁移教程-TODO这个好像不行.–必须使用mv命令,cp会丢失权限信息,可能会导致意想不到的bug

4.Docker数据目录(/var/lib/docker)迁移

go TryLock

#

参考

使用 unsafe 操作指针 实现自旋锁 使用 channel 实现 性能比较 参考资料

参考

1.为 Go Mutex 实现 TryLock 方法

Prometheus Metrics 设计的最佳实践和应用实例,看这篇够了!

背景

某个业务想增加一个监控指标。

就是每天不同渠道的老客唤起数量。 每日需要重置,然后更新,然后再累加。

而 Prometheus Metrics 有四种基本的 type:

Counter: 只增不减的单变量
Gauge:可增可减的单变量  // 看起来需要这个变量
Histogram:多桶统计的多变量
Summary:聚合统计的多变量

#

参考

1.Prometheus Metrics 设计的最佳实践和应用实例,看这篇够了!

mysql字符集

背景

有时需要调整某个表所有字段的字符集和排序规则。

# ALTER TABLE xxxx CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;

参考

1.

mac插件或工具

背景

参考

1.MacOS安装zsh插件zsh-autosuggestion(自动命令补全和建议)

redis-禁用高风险命令

背景

在业务场景中,无限制地允许命令使用可能带来诸多问题。一些Redis命令会直接清空大量甚至全部数据,例如flushall、flushdb等;keys、hgetall等命令的不当使用则会阻塞单线程的Redis服务,降低Redis服务的性能。

为保障业务稳定、高效率地运行,您可以根据实际情况禁用特定的命令,降低业务风险。

TODO 为什么要禁用keys

TODO 为什么要禁用hgetall

附录2,hscan

TODO 禁用之后使用什么命令和数据结构进行替代

#

参考

1.阿里云-redis-禁用高风险命令 2.使用Redis,你必须知道的21个注意要点

go版本对unmarshal的支持不同

背景

参考

大家使用工具生成 gorm结构体的时候,需要注意一下,生成的tag,多了一个primary_key字段;

比如:【ID int32 gorm:"column:id;primary_key" json:"id;primary_key"

go1.13 使用json marshal 的结果 与 go1.16 使用marshal 的结果 不一致,

开发 & 预发布是1.16, 一些旧的服务是编译用的 1.13(做需求时,需要尽快升级),混用的时候会出问题 升级两部 a 变更容器ID b build.sh 里 go build之前添加 go mod tidy

比如:预发布用1.16,通过json生成的缓存,在线上用1.13编译的服务读取这个缓存时是有问题的

规则引擎

背景

我在公司负责营销服务,经常有营销规则的变化。

目前想要引入一个规则引擎来提高研发效率。

参考

1.推荐一个新开源的规则引擎-ice 新的编排思想,轻量级,高性能 目前已在生产环境稳定度过了2年多,提高开发效率一倍多(之前主要用在营销活动上,一个简单的打榜活动从需求产出到上线可以控制在1h内)~这2年多线上错误率也降低了90%以上,越用越爽~ 致力于解决灵活繁复的硬编码问题,接入和使用成本相比同类产品Drools/Activiti低很多~也提供了一套配置规则的前端页面~快来看看吧~

linux shell或命令行

背景

最近看同事写的linux-shell脚本,发现好多语法看不懂.这里补习一下.

  1. shell函数调用

  2. 拼接cmdmingl

funHasString() {
  result=$(echo "$1" | grep "$2")
  if [[ "$result" != "" ]]; then
    return 0
  else
    return 1
  fi
}

heathCheck() {
  serverFlag=0
  canalFlag=0
  for ((i = 0; i < 25; i++)); do
    sleep 1

    st1=$("$CMD" -c "$CONF_FILE" status "$NAME_SERVER")
    echo "$st1"

    funHasString "$st1" "RUNNING"

     if [[ $? -eq 0 ]]; then
          echo 'server 启动成功'
          serverFlag=1
    fi

3.批量给某个目录下后缀sh的文件执行命令. // 附录2 // 附录5

# 批量给sh的文件赋权限
find . -name '*.sh' -exec chmod +x {} \;
  1. 使用 sed -i –和sed -i -e 搜尋與取代文字 // 附录3,6,7 批量替换sh文件中的内容 ```bash #对发布的文件/做一些处理,版本号替换,修改权限等 sed -i -e “s/BUILD_VERSION/$VERSION/” bin/*.sh

当我想替换文件中的路径时,使用转义符号(\)会比较麻烦且可读性很差。

比如要将abc.sh中的/a/b/c替换成/d/e/f,命令如下:

分隔符由/换成#(其他字符也可以,只要跟在s命令后面即可):

sed -i ‘s#/a/b/c#/d/e/f#g’


4.1 有的sed -i -e的参数末尾有个/g
// 附录4 页面搜索/g
```bash
sed -i -e 's/123/456/g' sedtest.txt

4.2 sed使用示例 // 附录4

5.将条件判断(if语句)写在bash命令行中

// 附录8

ps -ef | grep eureka |grep -v grep ;if [ $? == 0 ]; then ps -ef | grep eureka |grep -v grep |awk ‘{print $2}’|xargs kill -9; fi

Grep在匹配行之前显示文件名和行号

grep -n --with-filename 'pattern' file

参考

1.Linux–shell中$()命令 2.每天一个Linux命令(20)–find命令之exec 3.使用sed -i –和sed -i -e 搜尋與取代文字 4.Linux sed 命令–和使用示例 5.find <path> -name "*.sh" -exec chmod +x \{\} \; 是如何工作的? 6.sed替换内容中有斜杠该怎么处理 7.mac 执行sed -i指令时,总是出现extra characters at the end of command 8.将条件判断(if语句)写在bash命令行中 Linux(二):巧用分号在命令行执行逻辑判断语句 9.Grep在匹配行之前显示文件名?

RocketMQ一个新的消费组初次启动时从何处开始消费呢?

背景

使用mq报错如下

{"level":"warn","ts":"2022-01-13 20:48:11","logger":"ROCKET","msg":"delete mq from offset table","consumerGroup":"实例xxx%GID_Canal_price_t_product","MessageQueue":"MessageQueue [topic=实例xxx%aplum_t_product, brokerName=bjshare6-06, queueId=0]"}
{"level":"error","ts":"2022-01-13 20:48:11","logger":"ROCKET","msg":"fecth offset of mq from broker error","consumerGroup":"实例xxx%GID_Canal_price_t_product","MessageQueue":"MessageQueue [topic=实例xxx%aplum_t_product, brokerName=bjshare6-06, queueId=0]","underlayError":"broker response code: 22, remarks: Not found, V3_0_6_SNAPSHOT maybe this group consumer boot first"}

RocketMQ一个新的消费组初次启动时从何处开始消费呢?

最后发现是我启动go程序,启动mq subscibe后,程序直接退出了,在mq的日志离开看到上述报错。

看着报错日志很懵,是因为对apache rocketmq sdk的处理没有理解得很好。

参考

1.RocketMQ一个新的消费组初次启动时从何处开始消费呢?

FLUSH TABLES WITH READ LOCK

背景

今天数据团队: 大量抽取主库数据,导致线上部分数据库交互功能无法使用。

以下是同事写的事故报告和经验总结:

  • 事故说明:

今天下午14:40从主库同步部分RDS主库数据至CH,支持新需求的开发,但在同步前没有了解CH同步数据的机制,导致数据库不可用。CH同步数据的机制是使用”FLUSH TABLES WITH READ LOCK“,备份工具获取一致性备份(数据与binlog位点匹配)。 由于FTWRL总共需要持有两把全局的MDL锁,并且还需要关闭所有表对象,此次是这个命令导致库hang住。影响线上数据库交互业务。

  • 整改措施:

1、后续应避免从生产读写库同步数据,应从只读备库同步 2、后续在同步数据时需提前了解工具特性,思考同步机制可能带来的影响,并做好测试 3、如必须在生产库同步,应避在业务量最低的时候执行(凌晨4~5点) 4、数据同步方案应与上下游干系人提前沟通,并监控抽取后的一段时间

FLUSH TABLES WITH READ LOCK简称(FTWRL),该命令主要用于备份工具获取一致性备份(数据与binlog位点匹配)。由于FTWRL总共需要持有两把全局的MDL锁,并且还需要关闭所有表对象,因此这个命令的杀伤性很大,执行命令时容易导致库hang住。如果是主库,则业务无法正常访问;如果是备库,则会导致SQL线程卡住,主备延迟。

参考

1.FLUSH TABLES WITH READ LOCK

redis4.0新特性-非阻塞删除

背景

最近做商品全量静态化的线上测试.偶尔需要清空全量缓存。 因为之前有同事线上执行keys等命令导致redis不可用。所以我也不敢盲目flushdb。

知识点

// 附录1 Redis作为一个单线程模型的服务,当执行一些耗时的命令时,比如使用DEL删除一个大key(元素超大的集合类型key),或者使用FLUSHDB 和 FLUSHALL 清空数据库,会造成redis阻塞,影响redis性能,甚至导致集群发生故障转移。另外redis在删除过期数据或因内存超过容量淘汰内存数据时,也有可能因为大key导致redis阻塞。

为了解决以上问题,redis 4.0 引入了惰性删除lazyfree的机制,它可以将删除键或数据库的操作放在后台线程里执行,删除对象时只是进行逻辑删除,从而尽可能地避免服务器阻塞。

lazyfree使用 针对以上两种场景,redis分别新增了几个命令和配置选项,同时lazyfree的使用分为2类:第一类是与DEL命令对应的主动删除,第二类是过期key删除、key淘汰删除。

公司redis版本

redis-5.0_0.5.7

flushdb async

对于清空数据库命令flushall/flushdb,添加了async异步清理选项,使得redis在清空数据库时都是异步操作。

实现逻辑是为数据库新建一个新的空的字典,把原有旧的数据库字典给后台线程来逐一删除其中的数据,释放内存。

参考

1.[redis学习笔记]redis4.0新特性-非阻塞删除

【同事事故复盘】alter table修改枚举值导致数据被删除

背景

今天有同事整了个事故:

alter table修改枚举值,比如从a,b,c修改为 d,f,e,那么会删除掉旧的a,b,c的记录。

我看是同事写错了字段,把枚举字段A写成了另一个枚举字段B,然后B的枚举值,就覆盖到A字段了,相当字段A的枚举值被删除。

我一直以为alter table不会导致数据删除来着,同事的这个事故倒是弥补了我的盲区。

参考

1.MySQL精选 | 枚举类型ENUM的DDL变更测试-f. 修改枚举类型字段定义,删除某个枚举元素

bm25

#

参考

1.通过实战案例来讲解ES的query优化-提高搜索结果在类别上的相关性 2.实用 BM25 - 第 2 部分:BM25 算法及其变量 3.实用 BM25 - 第 3 部分:在 Elasticsearch 中选择 b 和 k1 的注意事项

micro-api网关协议转换的原理

背景

同事遇到一个问题:

proto文件定义的数据类型是int64,但是走micro-api服务收到的(输入或输出)变为了string?

micro-api网关协议转换的原理

参考

1.

干货 | Elasticsearch 向量搜索的工程化实战

背景

{
    "settings": {
        "number_of_shards": 3,
        "number_of_replicas": 2,
        "index": {
            "routing": {
                "allocation": {
                    "require": {
                        "node_group": "hot" // 1
                    }
                }
            },
            "store": {
                "preload": [ // 2)
                    "knowledge",
                    "category",
                    "available",
                    "confidence",
                    "del",
                    "kid"
                ]
            },
            "search": {
                "slowlog": {
                    "threshold": {
                        "query": {
                            "warn": "1s" // 3)
                        },
                        "fetch": {
                            "warn": "1s" // 3)
                        }
                    }
                }
            },
            "translog": {
                "flush_threshold_size": "512mb", // 4)
                "sync_interval": "5m", // 4)
                "durability": "async" // 4)
            },
            "sort": {
                "field": [ // 5)
                    "kid",
                    "confidence"
                ],
                "order": [ // 5)
                    "asc",
                    "desc"
                ]
            }
        }
    }
}

说明: 1.由于向量数据较大,所以倾向于将整个索引都放置在硬件性能更好的节点 2.为了支持高性能过滤,将常用的字段预先加载在内存中 // TODO 3.对慢查询开启日志方便后续性能问题的调查 4.知识库的重建是离线的,会在更新时进行大量写入,所以对 translog 的提交间隔拉长,加快写入速度 // TODO 5.在实际使用中kid是自增id,同时可能会对知识的置信度做排序等,所以会使用 sort field 存储这两个字段。 // TODO

从文章可以学到

1.提高过滤速度。将常用字段预先加入内存,空间换时间。

2.提高写入索引的速度。 增加translog刷盘间隔。

参考

1.干货 | Elasticsearch 向量搜索的工程化实战

Go的依赖注入框架

背景

公司内部分享了Kratos开源框架,其中有一些思想值得借鉴.

为什么需要依赖注入?

// 附录1

其中提到了依赖注入的2个好处

1.Redis 是基础组件,往往会在项目的很多地方被依赖, 那么如果哪天我们想整体修改 redis sdk 的甚至想把 redis 整体替换成 mysql 时,需要在每个被用到的地方都进行修改,耗时耗力还容易出错。

2.很难对 App 这个类写单元测试,因为我们需要创建一个真实的 redis.Client。

自己的思考:依赖注入的坏处

1.编码违背直觉? 2.可阅读性下降?

参考

1.从 Kratos 设计看 Go 微服务工程实践-Go 的依赖注入框架有两类 2.Fx: Golang中的依赖注入(Dependency Injection)工具(翻译)

awk的应用场景

背景

偶然看到一篇性能优化博客,里面记录了awk统计了小时内每分钟的平均调用时长。

cat 性能日志 | grep '时间点 | awk -F ' ' '{print $2, $5}' |.......| awk -F ' ' '{sum[$1]+=$3;count[$1]+=1}END{for(i in sum) {print i,sum[i]/count[i]}}'   

awk 打印出最后一行的第1列或者第2列?

打印最后一行的第二列就这样: awk ‘END{print $2}’ yourfile 打印指定行的指定列(例如第4行第2列): awk ‘NR==4{print $2}’ yourfile

从后往前

有时awk的print$是按照空格区分的,如果日志里有有个字段内容包含空格,则分割会错误。 有时发现可以从后往前分割就可以了。 // 附录6

grep|sed|awk 正则截取字符串

// 附录7,8

grep: 
echo ...|grep -oP 'word.*abcd'

sed:
echo ... |sed -r 's/.*(word.*abcd).*/\1/g'

awk:
echo ... |awk '{print gensub(".*(word.*abcd).*","\\1","g")}' 

实例

参数oP一起使用,会单独打印出要匹配的数字
[root@ss-server ~]# echo office365 | grep -oP "\d+"
365
[root@ss-server ~]# echo office365 | grep -oP "\d*"
365
[root@ss-server ~]# echo office365 | grep -oP "[0-9]*"
365
[root@ss-server ~]# echo 365beijing23 | grep -oP "\d+"
365
23
[root@ss-server ~]# echo 365beijing23 | grep -oP "\d*"
365
23
[root@ss-server ~]# echo 365beijing23 | grep -oP "[0-9]*"
365
23

实例:

  371  cat gin-http.log.2022-02-10 |grep 'eqq-notify'|grep -w 'old_user_tag=gdt-yfioslk'|grep -w 'app_type=ios'|grep -oP 'muid.*?&'> old_user_idfa.txt

匹配什么开头的字符串

echo "abcabc12312" |grep -oP "(?<=(abc)).*"

awk + uniq

awk -F ' ' '{print $2, $5}' | uniq -c

awk -F ',' '{print $1}' | uniq -c

TODO

# awk for循环统计 行数n,累加t+,平均值t/n  => 行数,耗时毫秒,平均耗时数
grep part3 main.log | grep '耗时' | awk -F ',' '{print $5}' | awk -F'"' '{print $4}' | awk -F"." '{n+=1; t+=$1} END{print n, t, t/n}'

awk + uniq

awk -F ' ' '{print $2, $5}' | uniq -c

awk -F ',' '{print $1}' | uniq -c

* linux之awk命令获取最后一列
awk -F',' '{print $NF}'

uniq + sort

Linux中sort和uniq关于排序去重的那些用法

* 统计耗时分布,需要先sort再去重

cat gin-http.log |grep -v '限流'|grep '2022-07-01 15:32:'|awk -F '"time":"' '{print $2}'|awk -F 'ms' '{print $1}'|awk -F '.' '{print $1}'|sort|uniq -c
 1 29
      9 30
      9 31
     17 32
     12 33
     21 34
     24 35
     25 36
     38 37
     57 38
     64 39

sort与uniq命令的使用

测试文本
aaa
bbb
aaa
ccc
aaa
eee
ddd

$ uniq -c data.txt
1 aaa
1 bbb
1 aaa
1 ccc
1 aaa
1 eee
1 ddd
# 每一行前都标注了出现一次,但是并没有进行统计
$ sort data.txt | uniq -c
3 aaa
1 bbb
1 ccc
1 ddd
1 eee
# 排序并进行了出现次数统计

sort -n -k 1

按照指定列排序

[aplum@recommend-content-serving01 logs]$ cat gin-http.log|grep recommend_content |awk -F 'uid' '{print $2}'|awk -F ',' '{print $1,$2}'|awk -F ':' '{print $2}'|awk -F ' ' '{print $1}'|sort|uniq -c|sort -n -k 1

awk数组简单入门

    $ cat file | awk '{a[$1]++}END{for (i in a){print i, a[i]}}'

awk的求和

awk '{sum += $1};END {print sum}' 
$NF 可用来表示一个记录的最后一个栏位
NF表示当前行的字段数,$0表示当前行

awk 求和与合并

求和题
文本文件test1.txt
a  12
b  25
c  23


awk 'BEGIN{sum=0}{sum+=$2}END{print sum;}' test1.txt 

实际使用

累计行数,同时打印某列的和。

awk -F "," '{n+=1; t+=$4} END{print n,t}'
合并题
文本test2.txt,每2行合并成一行
a 12
b 25
c 23
d 31

awk '{if( NR%2 != 0) ORS=" ";else ORS="\n"; print }'  test2.txt 

思路
1、如果行数等于基数,则输出分隔符为空白,
2、如果行数等于偶数,则输出分隔符为换行符,
3、打印全部

参考

1.解Bug之路-记一次线上请求偶尔变慢的排查–awk统计了下 B系统这个小时内每分钟的平均调用时长 2.使用awk命令获取文本的某一行,某一列 3.linux之awk命令获取最后一列 4.linux-awk中NF与$NF的区别 5.awk 打印出最后一行的第1列或者第2列? 6.AWK 技巧(取倒列,过滤行,匹配,不匹配,内置变量等) 7.grep|sed|awk 正则截取字符串 8.grep 正则截取字符串实例

DDD,领域驱动

背景

我们内部的一个管理系统,业务代码量超过100w行,遭遇了需求排期超过2个月的问题。 CTO开始试图寻找管控软件复杂度的方法论,结合业界经验,开始尝试领域驱动设计的方法论。

会议纪要

分享总结&&基本概念学习

大家再看一下这个图,方法论是Model-Driven-Disign,什么是Model?Model被表示为(Entity,Value Object,Service,Module)。 Entity 和ValueObject被封装为Aggregate(有一个Entity是聚合的Root),通过Factory来封装Entity和Aggregate的创建,用Repository来访问Entity和Aggregate。 通过分层架构(UI,应用,领域,基础设施)来隔离出领域层,UI/应用层/基础设施层不是领域模型关注的。上面的领域模型应用于分层架构中的领域层。

核心就是这么几句话了。

_config.yml

我的理解

因为我在3年前待的公司,主要是做微服务的二次重构(前期重要抢占市场,技术架构做得不好,业务发展还可以,所以才需要二次重构)。 当时公司的微服务包括Go和Java两个系列,另外当时我也了解了DDD,当时并没有找到结合实践的实现方式。

2021-12-22

// 附录2,3 发现一个很好的分享. DDD的实例

参考

1.领域驱动例子 2.COLA 2.0整体架构 https://toutiao.io/posts/9b2wg1d/preview 2.5.DDD的模式与实践案例 3.COLA

领域事件如何实施到topic和tag中

背景

电商商品流转过程中,商品状态很多,且有很多变化。 目前我们在商品状态时,会往MQ中投递一些消息。

因为topic是收费的,所以我们重要业务会有单独的topic。 其他业务都尽量使用1个topic,然后通过tag来区分消息,这样客户端在拉取的时候可以通过tag在MQ服务端就进行过滤。

现在的问题是: 其他业务使用1个topic,肯定是不合理的,tag会太多了。所以应该有多个topic。 那么自然就有如下问题: 哪些消息应该放到哪一个topic中。这个划分原则是什么?

目前的想法: 这个划分原则应该某种是业务领域。 划分业务领域,一是业务自身的边界,二是我们对业务的理解,三是按照领域驱动设计,其他的域划分。

不论是哪种方法,都需要我们对业务流程进行较为全面的梳理。

然后把业务流程划分为比较合适的子流程。 同时明确其中的领域事件。

整个过程不是一触而就的,需要在一开始设计时,就意识到,划分结果是不断变化和演进的。

这里给几个例子,说明topic和tag使用的典型情况

以下图电商交易场景为例,从客户下单到收到商品这一过程会生产一系列消息,以以下消息为例: 订单消息 支付消息 物流消息 这些消息会发送到Trade_TopicTopic中,被各个不同的系统所订阅,以以下系统为例:

  1. 交易topic + tags[order,pay,logistics]。整个业务流程,都被不同的消息,切割为不同的小段。这些消息暂且理解为领域消息吧。

我们面临的真实场景是

商品状态变化: 上架,下架,编辑中。

微服务的准备工作,需要充分。

1。一个必要的准备工作,应该是领域事件,更具体的,就是消息需要定下来。 并且这些消息要能推动整个流程的完成。// 场景测试。 2。评价准备工作的标准: 核心的场景流程定下来。 不管最终设计的结果,都需要用这些场景过一遍。

参考

1.topic-tag使用和消息过滤的实际场景

耦合的理解

背景

最近在做重构类型的工作,更多的偏向业务架构类型的工作内容。 据我总结,我们需要理解业务,划分有哪些模块,明确这些模块间的关系。才能具备设计一个的合理的架构设计的基础。

如何做到这些,需要一定的方法论进行支撑。

这篇博客列举和总结一些我觉得有用的方法论。

三个指标度量度量耦合

// 附录1 我建议,设计架构、考察模块之间关系时,不要用“耦合”、“乱”这些无法度量的词语, 而应该改用以下三个可以度量的指标:依赖、正交性、紧凑性。

代码耦合就是一方对另一方的假设,假设越多,两方的耦合度就越高

// 附录2 代码耦合的本质是一方对另一方的假设。两方之间的假设越多,两方的耦合度就越高。当然现实中,往往会遇到多方耦合。

参考

1.代码耦合是怎么回事呢? - 杨博的回答 - 知乎 2.代码耦合的本质是一方对另一方的假设

常见性能优化策略的总结

背景

本文要感谢我职级评定过程中的一位评委他建议把之前所做的各种性能优化的案例和方案加以提炼总结以文档的形式沉淀下来并在内部进行分享力求达到如下效果
1. 形成可实践可借鉴可参考的各种性能优化的方案以及选型考虑点同时配合具体的真实案例其他人遇到相似问题时不用从零开始
2. 有助于开阔视野除了性能优化之外也能提供通用的常见思路以及方案选型的考虑点帮助大家培养在方案选型时的意识思维以及做各种权衡的能力
文章在内部分享后引起强烈分享得到了不少同事和朋友的认可和好评觉得对日常的工作有很好的指导作用考虑到这些经验可能对业界同行也有帮助所以在美团点评技术团队博客公开

参考

1.常见性能优化策略的总结

Go-Micro Flag处理的问题

背景

参考

1.

Linux 指令详解 alias 设置别名

背景

参考

1.Linux 指令详解 alias 设置别名

redis通过pipeline提升吞吐量

背景

参考

1.redis通过pipeline提升吞吐量

MySQL优化实战-冷热分离

背景

目前接手过的业务, 优惠券和广告,都存在单表大表的的情况。

优惠券: 单表5亿+,大几十G。查询操作较多: 发券,领券 都需要进行查询和写入操作。

广告: 单表 5kw+,40G+。 CRUD qps > 1000。

参考

1.MySQL优化实战:冷热分离

redis hash的使用场景

背景

最近在做电商商品列表页和详情页的价格缓存. 在设计缓存数据结构时,用到了hash结构。

这里记录一下redis hash的主要的使用场景.

具体参考附录1,非常详细的描述了redis的hash解决的问题,以及其底层的存储结构。

redis zset使用场景 TODO

背景: 存放用户搜索的主词和后继词.

key:主词 后继词s:按照CTR排序

r-xxx.redis.rds.aliyuncs.com:6379[11]> ZRANGE 劳力士 0 -1 WITHSCORES
男表
5.0097563200000002
手表男
5.1728499799999996
女表
5.2558560600000002
表
5.3265461399999996
劳力士男
5.7810733499999998
机械
5.7932277499999998
手表女
5.8832969500000001
劳力士女士日志
6.3485439599999998
劳力士女士
6.55728651
劳力士迪通拿
6.7381185199999996
劳力士间金
6.7414224999999997
劳力士 黑色
6.7624214499999997
劳力士36
6.7869144600000002
劳力士绿水鬼
6.7884882400000004
劳力士水鬼
6.8178139
劳力士 游艇
6.8229029700000003
劳力士鬼王
6.8240750099999996
劳力士日志
6.9103165100000004
手表
7.6918011399999999
包
9.7704448900000003

参考

1.Redis 几种数据类型及应用场景 2.键值对采用不同类型保存时的内存开销

深入golang runtime的调度

背景

技术群遇到一个面试题分享:

Q: 为什么优先从p的freeg中获取,这样做有什么优点?

他描述的问题应该是为什么优先从本地队列拿,而不是steal或者全局。

当一个g执行完成之后,并不是直接收回g的内存空间,而是把g加入到p所对应的freeg列表当中,这就是空闲的g。

操作全局队列要加锁。如果是为啥要从本地队列里面拿,好像是为了避免多个p竞争锁?

参考

1.深入golang runtime的调度 2.Golang深入理解GPM模型

runtime.Gosched()函数

背景

我在线上使用这个库的代码,在看该库的实现的时候,发现了不常见函数的使用场景, 提了一个issue,想知道为啥这么用。

func (agt *Aggregator) TryEnqueue(item interface{}) bool {
	select {
	case agt.eventQueue <- item:
		return true
	default:
		if agt.option.Logger != nil {
			agt.option.Logger.Warnf("Aggregator: Event queue is full and try reschedule")
		}

		runtime.Gosched() // 这个函数的作用是让当前goroutine让出CPU,好让其它的goroutine获得执行的机会。同时,当前的goroutine也会在未来的某个时间点继续运行。
                //  过一会儿再次调度到这个携程,继续往下执行。要么入队成功,要么返回入队失败。
		select {
		case agt.eventQueue <- item:
			return true // 入队成功
		default:
			if agt.option.Logger != nil {
				agt.option.Logger.Warnf("Aggregator: Event queue is still full and %+v is skipped.", item)
			}
			return false // 入队失败
		}
	}
}

runtime.Gosched()使用场景

这个函数的作用是让当前goroutine让出CPU,好让其它的goroutine获得执行的机会。同时,当前的goroutine也会在未来的某个时间点继续运行。

除了让出CPU,还可以通过自旋来等待一会。自旋的方式可以避免携程上下文的切换。

那么这样就会引出一个问题:什么时候该自旋,什么时候该让出当前携程? TODO ?

自旋锁的应用场景

// 附录2 自旋是自旋锁的行为,它通过忙等待,让线程在某段时间内一直保持执行,从而避免线程上下文的调度开销。自旋锁对于线程只会阻塞很短时间的场景是非常合适的

参考

1.go runtime.Gosched()函数解析 2.自旋锁的应用场景

Go依赖冲突问题

背景

遇到了附录1博客的问题。但是按照同样的修改,发现不行,所以需要进一步分析问题。

所以需要了解go.sum机制。//附录2

参考

1.解决 etcd 与 grpc 不兼容问题 2.谈谈go.sum

es配置升级的经历

背景

主要监控数据来源

1.阿里云工作台的监控 2.kibana内部的Monitoring

第一次升级

我们从 2核8g 2节点,升级到 2核8g 3节点,性能一点没变。 老配置运行了2年以上的时间,基本没有问题。

这次升级,就是因为2节点存在脑裂问题。

这里有2点需要关心:

Q1. 为什么可以2节点可以运行2年多,很少出问题? // TODO 附录1: minimum_master_nodes设定对你的集群的稳定极其重要。设置成1可以保证集群的功能,但是就无法防止集群脑裂了

GET /_cluster/settings
响应如下:
{
  "persistent" : {
    "cluster" : {
      "routing" : {
        "allocation" : {
          "node_concurrent_incoming_recoveries" : "1",
          "node_concurrent_recoveries" : "1",
          "exclude" : {
            "_ip" : ""
          },
          "enable" : "all"
        }
      }
    },
    "search" : {
      "isolator" : {
        "enabled" : "true"
      },
      "max_buckets" : "10000"
    },
    "monitoring" : {
      "collector" : {
        "kmonitor" : {
          "enabled" : "true"
        }
      }
    },
    "discovery" : {
      "zen" : {
        "minimum_master_nodes" : "2" // 这里的配置
      }
    }
  },
  "transient" : {
    "cluster" : {
      "routing" : {
        "allocation" : {
          "node_concurrent_incoming_recoveries" : "1",
          "cluster_concurrent_rebalance" : "6",
          "node_concurrent_recoveries" : "1",
          "exclude" : {
            "_ip" : ""
          }
        }
      }
    },
    "indices" : {
      "recovery" : {
        "max_bytes_per_sec" : "50mb"
      }
    }
  }
}

Q2. 2节点时出问题怎么办的? // TODO

Q3. 为什么又要升级为3节点了? 1.试图加节点来提升性能,升级后基本性能没有提升。// 核心需求 2.防止脑裂。这个不是刚性需求。

第二次升级

过了半年,再次升级 为 4核8g 三节点。 搜索耗时,从30ms减为15ms。

升级原因: 注意是看的es节点的负载。 4核机器,负载能达到3-4左右,所以负载较高

升级前情况:

  1. cpu使用率: 高峰能达到80-90%。明显吃紧。
  2. 负载: 4核,负载高峰能到3-4。明显吃紧。 // 负载=4表示资源100%用完
  3. 内存: FullGc基本没有,说明内存不吃紧。 同时还有heapMemery使用率
  4. 磁盘: 磁盘使用率很低<30%,不吃紧。

kibana内部的Monitoring监控数据

从kibana可以看到es节点的监控,升级后监控(cpu使用率和负载)如图:

_config.yml

es机器配置升级后搜索延迟为10-15ms,升级前是30-40ms(升级的忘记截图了)

_config.yml

阿里云工作台监控数据

es机器配置升级前后-节点负载对比

_config.yml

es机器配置升级前后-cpu使用率对比

_config.yml

内存和磁盘都够用 _config.yml

第三次升级 TODO

2022年10月,升级cpu,4核8g三节点 -> 8核8g三节点

参考

1.Elasticsearch: 权威指南 » 管理、监控和部署 » 部署 » 重要配置的修改-最小主节点数

RocketMQ在面试中那些常见问题及答案+汇总

背景

阅读同事代码时,发现了这个库. 预计最近一个项目能用到. 记录一下.

// 附录1 很多时候,解析来自多种源头的数据流时,我们一般事先并不知道他们对应的具体类型。只有读取到一些字段之后才能做出判断。

扩展

实际上,Google Protobuf 通常也使用这种方式。 在协议中添加消息 ID 或全限定消息名。接收方收到数据后,先读取协议 ID 或全限定消息名。 然后调用 Protobuf 的解码方法将其解码为对应的Message结构。

从这个角度来看,mapstructure也可以用于网络消息解码,如果你不考虑性能的话😄。

#
1.Go 每日一库之 mapstructure

RocketMQ在面试中那些常见问题及答案+汇总

背景

Q:8、消费消息是push还是pull?

Q: 追问:为什么要主动拉取消息而不使用事件监听方式?

Q: 当消费负载均衡consumer和queue不对等的时候会发生什么?

Q:

// 附录2

Q: TODO

// 附录3 消费者原理分析-RocketMQ知识体系(四) 前文了解了 RocketMQ消息存储的相关原理,本文将讲讲消息消费的过程及相关概念。

#
1.RocketMQ在面试中那些常见问题及答案+汇总

2.RocketMQ——消息ACK机制及消费进度管理

3.消费者原理分析-RocketMQ知识体系(四) RocketMQ 的基础概念和架构-RocketMQ知识体系1

慢sql日志中出现大量Commit语句

背景

线上报错

更新商品时出现 invalid connection 的错误.

查看阿里云慢sql日志,发现都是COMMIT语句,看不到sql,如图: _config.yml

_config.yml

查看自动提交配置

_config.yml

原因猜测

DB默认是自动提交的,不存在”COMMIT”; 既然是commit,肯定是会话被关闭了自动提交;

但是为什么没有记录对应的语句,而只记录了COMMIT;呢 ?

1.IO不足导致COMMIT出现在SLOW LOG中. 2.业务启用了大事务, 例如BEGIN开启事务后,里面执行了大量的INSERT语句,然后再做COMMIT提交;

验证 TODO

#
1.mysql慢日志都是COMMIT 2.COMMIT IN SLOW LOG浅析

RocketMQpush模式的处理过程

背景

什么时候用RocketMQ队列,什么时候用redis队列

或者说持久化,不丢消息,何时仍MQ,何时仍redis(list,hash)

#
1.模式: 安全的队列 2.Redis 消息队列的三种方案(List、Streams、Pub/Sub)

RocketMQpush模式的处理过程

背景

最近在做一个较大的性能优化,就是把商品的价格从实时计算改为离线计算。 这需要捕捉到所有影响商品价格的因子,这些因子主要是mysql的表. 所以考虑使用我们现有的canal来捕捉binlog变化,进而刷新价格.

但是商品数量是比较大,大几十万,不知道canal投递MQ的性能和消费的性能如何,所以需要测试。

RocketMQ有pull,puss两种消费模式。

pull每次能拉16

push则不受限制

TODO push模式的处理过程

附录1,2,3 // 附录4

  1. 而push方式是broker主動推送消息,但是push方式的底層實質是異步去broker上拉取消息,然後主動推送給consumer。 // http://hk.noobyard.com/article/p-hyydxnqw-ty.html

2.整个push模式的处理过程,其中几个关键点: // 附录2 通过pull模式实现 主动发起消息的接取,RebalanceImpl类的子类RebalancePushImpl是拉取消息的入口 在rebalance后会触发消息的拉取 拉取成功后,调用listener的consumeMessage方法

# 1.消息队列推拉push/pull模式优缺点及使用改进的长轮询模式的意义 2.RocketMQ的push模式机制 3.MQ中Pull和Push的两种消费方式 4.首页>消息队列RocketMQ版>最佳实践>消息堆积和延迟问题-产品详情-消息堆积和延迟问题

高并发情况下的业务错误记录(mysql)

背景

负责的广告下发服务,单机qps>400,遇到1个稍微有点特别的问题.

代码逻辑很简单

1. 查找是否存在任意一条设备did=x的记录. // select id from t where did = x limit 1;
2. 假设找到id=n的一条记录,则删除这条记录. // delete from t where id = n;
3. 然后插入一条新记录did=x // insert into t(did) values (x);

理论上,按照上述逻辑,t表只会存在1条did=x的记录.

但是从线上数据看,存在大量did=x的记录,并且这些记录在各种时间点都存在,没有规律。

原因分析

预备知识

delete操作是当前读,在mysql内部分为4个步骤执行

  1. current read

  2. return && lock

  3. delete row

  4. success

  • 参考delete操作在mysql内部的执行流程图

_config.yml

bug出现过程

在广告被刷量的情况下,单个设备did=x可能会在1秒内,请求1000次.

极端情况,可能1000个携程都执行到了step1,但是只有1个携程(1)成功,其他携程会因为行锁阻塞在step2上。

待携程(1)执行完step2,其他携程依次执行step2(删除不报错,但是RowsAffected=0),step3,step4

所以虽然执行了1000次删除语句,但是只删除了1条记录,但是我们最多可能写了999条记录。

有赞TCP网络编程最佳实践+网络指标监控

TODO

1.监控平台需要新增指标

2.限制最大连接数

// 附录2

# 1.16. 完善网络指标监控 + 7. 服务端需要限制最大连接数 2.golang官方实现如何对httpserver做频率限制(最大连接数限制)

普通索引 & 唯一索引 区别

TODO

# 普通索引 & 唯一索引 区别:

select 忽略不计 主要体现在update,普通索引不需要判断唯一性约束 change buffer,准确的说不在buffer中,有实质的差异,普通索引具备加速作用,在写多读少的场景

InnoDB redo、undo、binlog,是如何合作的

TODO

# InnoDB redo、undo、binlog,是如何合作的

MySQL锁(二)表锁:为什么给小表加字段会导致整个库挂掉?

TODO

# MySQL锁(二)表锁:为什么给小表加字段会导致整个库挂掉?

一文搞懂如何实现 Go 超时控制

背景

头条的超时控制

gorm的超时控制

gorm的超时控制

服务间的超时控制

附录

1.一文搞懂如何实现 Go 超时控制

Nginx的URL编解码处理机制

背景

公司发展还可以,业务和系统规模有了较大增长,技术团队得到扩招。

某些业务也需要完善的分布式事务方案来支持,不能只靠定时任务扫表这种替代方案来搞。

这也标志着公司的微服务化改造开启了从60分到90分的新进步。

seata-go

附录

1.第 93 期 2020-06-25 Seata Go 分布式事务框架的介绍与实践

Nginx的URL编解码处理机制

背景

重构时,从旧服务对nginx拷贝日志中的url出来,验证新服务能获取同样的结果。

在Gin框架的bind方法发现,ua参数不能正确的bind.

from=douyinxingtu&os=1&ua=Mozilla%2F5.0+%28iPhone%3B+CPU+iPhone+OS+14_6+like+Mac+OS+X%29+AppleWebKit%2F605.1.15+%28KHTML%2C+like+Gecko%29+Mobile%2F15E148&ip=112.111.55.167&ts=1626883143543&model=iPad11,3&demand_id=6966874113549795332&order_id=6966874119816085598&item_id=6976127045222911238&callback_param=EJiw267wvfQCGKf2g74ZIPD89-vIATAMOAFCIjIwMTkxMTI3MTQxMTEzMDEwMDI2MDc3MjE1MTUwNTczNTBIAQ==&callback_url=https://ad.toutiao.com/track/activate/?callback=EJiw267wvfQCGKf2g74ZIPD89-vIATAMOAFCIjIwMTkxMTI3MTQxMTEzMDEwMDI2MDc3MjE1MTUwNTczNTBIAQ==&os=0&muid=db94e6a60a9c6661e3e03a7d301c25b6”

from=douyinxingtu os=1 ua=Mozilla%2F5.0+%28iPhone%3B+CPU+iPhone+OS+14_6+like+Mac+OS+X%29+AppleWebKit%2F605.1.15+%28KHTML%2C+like+Gecko%29+Mobile%2F15E148 ip=112.111.55.167 ts=1626883143543 model=iPad11,3 demand_id=6966874113549795332 order_id=6966874119816085598 item_id=6976127045222911238 callback_param=EJiw267wvfQCGKf2g74ZIPD89-vIATAMOAFCIjIwMTkxMTI3MTQxMTEzMDEwMDI2MDc3MjE1MTUwNTczNTBIAQ== callback_url=https://ad.toutiao.com/track/activate/?callback=EJiw267wvfQCGKf2g74ZIPD89-vIATAMOAFCIjIwMTkxMTI3MTQxMTEzMDEwMDI2MDc3MjE1MTUwNTczNTBIAQ== os=0 muid=db94e6a60a9c6661e3e03a7d301c25b6”

附录

1.Nginx的URL编解码处理机制

什么时候用NoSql

背景

最近将广告下发从php迁移到了go,以微博为例,请求qps最大可能能超过1k,如果算上其他的几个渠道,qps峰值可以超过3k.

每个请求基本涉及1次数据库插入,3k/s的速度写入我们线上的polardb,基本就把机器挂了。

针对这个问题,虽然已经根据业务特点,采用了异步写入+批量写入的优化方式,把对数据库的写入操作qps减少了N倍(N=每批量的写入数量)。

当时出方案时,考虑了异步写入的弊端,就是异步写入总是有可能存在不一致的(回复写入成功,但是程序崩溃了)。 就是说如果业务不允许异步写入,那要怎么办呢,如何可以保证100%的一致性呢。

今天我在看一篇博客时(附录2),看到了nosql相关的场景描述,发现这也是一个方案(我这里特指mongodb). 在我遇到的场景中:

nosql的优势:写入数据很快(待确认),存储的数据库很大()

nosql的劣势:查询语法学习成本,公司未使用mongodb需要新购买和维护

附录

1.Sql Or NoSql,看完这一篇你就懂了

2.搞定系统设计 01:从 0 到百万用户的系统:下面这几个场景更适合使用 NoSQL: 服务需要超低延迟。 服务的数据是非结构化的。 只需要序列化反序列化数据(Json/XML/YAML) 需要存储大量数据。

Nginx失败重试机制

背景

我负责的广告下发业务. upstream模块配置转发到4台机器,后来我停止了1台机器,但是忘记改upstream配置了。

这个错误到配置持续了2天多,直到今天周一才发现,但是业务却没有任何问题。

这引起了我的困惑。

和同事简单讨论,了解到nginx有重试机制。

猜想应该是这个原因才使得业务没有出现。

另外,我也考虑增加一些监控,使得能够及时发现错误的配置信息,主要思路是监控nginx的错误日志(error.logk)

错误配置时,错误日志如下

2021/10/18 16:48:29 [error] 2828#0: *88397155 connect() failed (111: Connection refused) while connecting to upstream, client: 49.7.38.70, server: open.aplum.com, request: "GET /adds/weibo-notify?微博广告监测链接", host: "xxx.xxx.com"

重试的具体过程(待验证)

If an error occurs during communication with a server, the request will be passed to the next server, and so on until all of the functioning servers will be tried. If a successful response could not be obtained from any of the servers, the client will receive the result of the communication with the last server.

附录

1.Nginx失败重试机制 2.Module ngx_http_upstream_module

Consul注册中心(TODO)

背景

consul是之前同事部署的,我不熟悉,最近遇到相关问题,发现还是需要学习一下子.

学习路径

  1. consul在微服务中的作用

  2. consul架构 和 部署结构

  3. go-micro涉及的consul相关的代码

  4. go-micro的服务注册和发现组件

FAQ 2021-10-20

Q:consul部署命令中的join是什么意思 (TODO)

// 附录2

consul agent 分为 server 和 client, 一个节点2种角色.

我们分析一下,为什么第5步和第6步需要加-rpc-addr 选项,而第4步不需要加任何选项呢? 原因是-client 指定了客户端接口的绑定地址,包括:HTTP、DNS、RPC,而consul join 、consul members 都是通过RPC与Consul交互的。

docker环境搭建consul

1台机器和多台机器的情况

启动第一个节点 docker run -d –name consul-1 -p 8300:8300 -p 8301:8301 -p 8302:8302 -p 8500:8500 -p 8600:8600 consul:1.9.8 agent -server -bootstrap-expect 1 -ui -bind 0.0.0.0 -client 0.0.0.0 -node node-1 -enable-script-checks=true ———————————————— 版权声明:本文为CSDN博主「灬倪先森_」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/Lyon_Nee/article/details/119324603

搭建 consul 集群 Docker - 容器部署 Consul 集群 Consul with Containers

附录

公司内网的部分资料

1.安装Consul集群 2.使用Consul做服务发现的若干姿势

Go项目中日志处理的注意事项

背景

在实际生产实践中,日志相关的问题出现过很多次,这里把问题和方案做一次梳理

QA

Q: 哪些东西应该记录日志.

A: 以可以还原业务流程,便于分析定位线上问题,为准.

监控性质和非业务日志应该尽可能少

A: 项目初期上线时,未知bug可能较多,日志尽可能的丰富一些. 项目稳定后可以逐渐减少日志.

Q: 日志内容很多怎么办

A: 建立软连接把日志文件链接到大容量的磁盘. // 存储相对便宜,对于大部分项目,这个成本任何规模的公司都可以接受。

Q: 对于并发量大的项目的日志,日志不是顺序打印的,难以跟踪单个请求的完整业务日志.

A: 需要给每个请求增加唯一id,在日志中打印出来,用来串联一个请求的完整业务日志.

Q: 调试性质的日志,如何处理

A: 算法调试,

附录

1.golang 日志 zap的使用

面试题 && 同事互问题

背景

1.技术群有新人提问: 有大佬能用简单易懂的方式向面试官解释倒排索引吗?

2.有个新同事新接触es,问我倒排索引怎么理解.

3.之前有同事面试,也被问过该问题, 所以写篇博客记录一下.

我的回答

一本字典,我给你一个字,你需要告诉我这个字在字典的哪一页。 我理解这就是正排索引。

一本字典,我给你一个词,你需要告诉我这个词在字典的哪些页出现了。 你需要设计一个怎么样的数据结构来处理这个问题。 这就是倒排索引。

附录

1.官网-倒排索引 2.漫画趣解:透析Elasticsearch原理

Golang的time.NewTimer单次定时器使用案例

背景

附录

1.[Go] 基础系列一: for-select中的break、continue和return

面试题 && 同事互问题

背景

  1. for和select一同使用,有坑。break只能跳出select,无法跳出for

附录

1.[Go] 基础系列一: for-select中的break、continue和return 2.漫画趣解:透析Elasticsearch原理

wildcard的使用场景,弊端,替代方案

背景

主要有2个场景会用到wildcard,但是处于性能考虑,都没有采用wildcard的方案

  1. 用户搜索的场景: 因为词典导致分词结果不一致,此时可以用wildcard进行类似mysq like的方式来召回。

  2. 公司内部运营选品的场景: 希望输入n个词语,然后召回商品名字包含这些词的哪些商品,这些词之间是或的关系。 比如: 输入 a|b|c 三个词, 希望召回商品标题里面包含a或b或c的那些商品.

这个走分词搜索的逻辑,仍然会有词典导致的分词问题,所以考虑wildcard。 不过因为性能的顾虑,最终放弃了该方案,最终方案,是拆分为n次搜索,每次一个词语召回商品,然后合并商品,

#

附录

1.Elasticsearch 警惕使用 wildcard 检索!然后呢?

面试题-如何实现分词-原理是什么

背景

有个同事出去面试,遇到了这个问题

分词主要有2类思路

  1. 传统算法(前缀树算法+DAG算法): 我们在搜索项目中应用过基于字典树(也就是前缀树)的一个简单分词器

  2. 机器学习算法:

附录

1.结巴分词原理讲解

2.NLP分词

3.NLP分词-本地PDF

如何用 Golang 的 channel 实现消息的批量处理

背景

我负责迁移的广告下发服务,提供广告下发的监测链接接口. 流量波动特别,qps多时可以上千。旧服务是每次接口调用都写1次数据库,导致出现了线上事故,所以我这边进行优化。

优化思路:

将下发消息进行聚合后,批量写入数据库,这样可以极大的减少数据库操作.

附录

1.如何用 Golang 的 channel 实现消息的批量处理 2.源码

为什么会精度丢失?

背景


f := float64(2.9) + float64(1.11) - float64(4.01) fmt.Println(f)

线上结果是float64(-4.440892098500626e-16),本地是0

原因

和编译器有关系

同样的操作 你在mac下面和linux下面可能数据不一样

#

处理方案

附录

1.为什么会精度丢失?教你看懂 IEEE-754

Linux软连接和硬链接 && mv命令

发现磁盘空间不足时,需要的做软链接

  1. 停止服务,备份当前日志文件
  2. 删除当前日志目录,改为创建软链接到新挂载的大磁盘目录中
  3. 这样应用进程就会写到新磁盘中。

TODO

可能的问题

  1. 需要停止服务

  2. 能否用脚本一键操作?

#  停止服务 
sh deploy/run.sh stop 
# 在新磁盘创建对应日志目录
mkdir -p /data/aplum/price-srv-logs/logs && mkdir /data/aplum/price-srv-logs/runtime && mkdir /data/aplum/price-srv-logs/runtime_canal
# 移动旧日志
mv logs/ /data/aplum/price-srv-logs && mv runtime/ /data/aplum/price-srv-logs && mv runtime_canal/ /data/aplum/price-srv-logs
# 当前目录创建软链接(logs,runtime,runtime_canal),指向新磁盘日志路径
ln -s /data/aplum/price-srv-logs/logs logs && ln -s /data/aplum/price-srv-logs/runtime runtime && ln -s /data/aplum/price-srv-logs/runtime_canal runtime_canal && sh deploy/run.sh start

ln -s /data/aplum/product-logs logs
# 验证下磁盘使用率
cd ~ && du -h --max-depth=1
df -lh

mv命令:移动或改名,

取决于目标目录是否存在。存在则移动,不存在则无法移动就是改名 附录5

mv source_directory(目录) dest_directory(目录)	

1.目录名 dest_directory 已存在,将 source_directory 移动到目录名 dest_directory 中;
2.目录名 dest_directory 不存在则 source_directory 改名为目录名 dest_directory

附录

1.Linux软连接和硬链接 2.图示软硬链接的区别 3.mv命令是move的缩写,可以用来移动文件或者将文件改名(move (rename) files),是Linux系统下常用的命令,经常用来备份文件或者目录。 4.linux 软连接的使用 5.Linux mv 命令

服务超时:接口,mysql

背景

今天线上除了事故导致APP挂掉. 原因是阿里云数据库被大量查询访问导致超载,响应慢。

我们的调用链路是:

1.APP -> 2.php服务端 -> 3.Go基础服务端 -> 4.阿里云数据库

附录1描述了取消释放资源的不同情况 TODO

关于超时,可以把开发者分为两类:一类是了解超时多么难以捉摸的人,另一类是正在感受超时如何难以捉摸的人。

附录

1.使用 timeout、deadline 和 context 取消参数使 Go net/http 服务更灵活

nginx作用,安装(略),目录结构说明,使用案例

背景

最近有个新服务需要部署,不属于之前的微服务体系,是离职的算法同事的项目,侧重数据离线计算的一个项目.

部署需要用到nginx做轮询转发,所以学习nginx。工作四年才在线上用到nginx,所以现在才专门学一下。。。

主要内容

  • nginx作用,安装(略),最佳实践,配置(Nginx目录建议),目录结构说明,使用案例

# 安装 Nginx 时可以选择配置文件的路径,由于 Nginx 程序后续可能会升级版本,但配置基本上就是一份,那么推荐使用配置和程序分离的方式,遵循:

配置文件独立管理, 不存放在nginx程序目录内 每个站点独立一个配置文件 每个站点独立的日志文件 提取公用的配置文件

附录

1.Nginx 目录建议

面试题-sync.Map 和 map+读写锁区别是啥

背景

附录

1.sync.Map 和 map+读写锁区别是啥 2.深度解密 Go 语言之 sync.map

go1.16升级编译失败问题

背景

go1.13升级为go1.16后,go build报错如下:

/usr/local/go/pkg/tool/linux_amd64/link: running g++ failed: exit status 1
/usr/bin/ld: /tmp/go-link-852636201/000012.o: unrecognized relocation (0x2a) in section `.text'
/usr/bin/ld: final link failed: Bad value
collect2: error: ld returned 1 exit status

附录

1.go1.16升级编译失败问题_luckydoit的博客-程序员宅基地

GO编程模式PIPELINE (Golang并发模型:流水线模型)

背景

推荐算法的同事交接了部分算法的项目给我。

该部分业务的流程类似如下逻辑:

本来的实现是:

发现可以采用这个模式进行处理,提升性能。

Q:什么业务可以 fan out and fan in(merge)

FAN-IN和FAN-OUT模式

// 附录2 FAN-OUT模式:多个goroutine从同一个通道读取数据,直到该通道关闭。OUT是一种张开的模式,所以又被称为扇出,可以用来分发任务。

FAN-IN模式:1个goroutine从多个通道读取数据,直到这些通道关闭。IN是一种收敛的模式,所以又被称为扇入,用来收集处理的结果。

FAN模式一定能提升性能吗?

流式计算与批量计算

// 附录3

附录

1.GO编程模式:PIPELINE 2.Golang并发模型:流水线模型 3.流式计算与批量计算

面试-redis的string类型的数据,get和set为什么时间复杂度是O1

在redis中,所有的键值对都会用内置的哈希表保存在内存里??? TODO

附录

2021-8-30-go1.16.md

背景

GET /xxxx/product/_search
{
  "_source":false,
  "from": 0,
  "query": {
    "bool": {
      "filter": [
        {
          "term": {
            "visible": 1
          }
        },
        {
          "term": {
            "is_es_show": 1
          }
        },
        {
          "bool": {
            "should": [
              {
                "terms": {
                  "status": [
                  ]
                }
              },
              {
                "term": {
                  "liveapp": 1
                }
              }
            ]
          }
        }
      ]
    },
    "size": 10,
    "sort": [
      {
        "liveapp": {
          "order": "desc"
        }
      }
    ]
  }
}

报错信息

{
  "error": {
    "root_cause": [
      {
        "type": "parsing_exception",
        "reason": "[bool] malformed query, expected [END_OBJECT] but found [FIELD_NAME]",
        "line": 50,
        "col": 5
      }
    ],
    "type": "parsing_exception",
    "reason": "[bool] malformed query, expected [END_OBJECT] but found [FIELD_NAME]",
    "line": 50,
    "col": 5
  },
  "status": 400
}

知识点

// 附录1 1.filter工作于bool查询内。 工作原理是: 查询过程中,优先经过filter过滤,因为should是或关系,龙套偏房和魏行首的年龄符合了filter过滤条件,也就被放行了! 2.filter与bool平级,会报错。

附录

1.Elasticsearch-布尔查询 -> 注意:filter工作于bool查询内。比如我们将刚才的查询条件改一下,把filter从bool中挪出来

升级go版本1.13->1.16

背景

公司最近打算升级go版本1.13->1.16,需要新做1个阿里云的镜像.

附录

1.golang 1.14 1.15 1.16 新特性一览

日常吐槽 限流熔断如何做,谁来做

背景

技术群讨论,某知乎工程师吐槽:基础架构组没有提供熔断功能,甩锅给业务组.

附录

1.Golang微服务:Micro限流、熔断

2.谈谈我对服务熔断、服务降级的理解 专题

共识算法:PBFT(拜占庭容错)图解

背景

日常技术聊天,看到一个图画的很好,很好的解释了 PBFT(拜占庭容错)

附录

1.共识算法

2.块链共识算法 PBFT(拜占庭容错)、PAXOS、RAFT简述

面试题:一个进程最多可以申请多少物理内存?

背景

技术群讨论,面试题

Q:问各位一个问题:4g服务器,一个进程最多可以申请多少物理内存(不是虚拟内存的寻址范围)

A: 我记得可以申请无限制的 先申请无限的,然后缺页时加载到物理内存,如果物理内存满了,那就交换到磁盘里面去?

最多申请: 磁盘+物理内存 那么多?

B: 进程只能申请虚拟内存

C: 不,就是问实际物理,我回答虚拟内存,寻址范围,动态段页分配(就是虚拟内存到物理内存的映射)这些点都被面试官驳回了. [抠鼻]搞不懂他到底想问什么

TODO

linux内存工作原理

// 附录1 _config.yml

如何查看内存使用情况

// 附录1

  • 64位的Linux默认占用256TB中的128TB,程序只能使用剩下的128TB。 // 附录2

附录

1.15 | 基础篇:Linux内存是怎么工作的?虚拟地址(内核空间和用户空间)+缺页异常+物理内存 2.一个程序最多可以使用多少内存?-操作系统还要占用内存 3.Linux 内存相关问题汇总-malloc申请内存的流程-TODO

面试题:系统设计

背景

技术群讨论,面试题

Q: 提问:一个存储系统,包括缓存和持久化存储,用DDD的思想落地,提供接口给外部调用,注意事项:有可能会更换存储引擎。

Q: 提问:实现一下ab之间的扫码支付功能。安全,流量,一致性等问题都要考虑。

Q: 提问:实现一个定时回调系统,外部接口提供注册回调事件功能,数据规模一天500w,需要按照用户注册的时间,比如未来30分钟,后天0点,等,类似延时队列。

A: 类似我们内部做的生产,消费者服务. 调用注册回调接口,我们来调用回调接口,完成消息的消费.

es搜索权重调整的几种方式

背景

我负责电商的商品搜索,经常遇到这样的需求:当商品参加某个活动或满足某个条件时,希望可以在用户搜索时进行加权,曝光更靠前.

比较常用的实现思路: 1.往es写入1个字段标记是否满足某个条件 2.搜索时对这个字段,进行查询时权重调整. 2.1 调整字段weight 2.2 将function_score查询与field_value_factor结合使用 // 附录2

主要内容

1.基于字段的权重调整

  1. 在搜索时,可以将function_score查询与field_value_factor结合使用

2.其他权重调整

控制得分在固定区间

// 附录2

附录

1.我们希望将更受欢迎的博客搜索结果列表中相对较上的位置–在搜索时,可以将function_score查询与field_value_factor结合使用,即将点赞数与全文相关度评分结合: 2.实战|Elasticsearch自定义评分的N种方法-有没有办法让同一个索引里面对固定的查询返回的相关性评分是在固定的范围之内的?比如0-100分这样的?

小顶堆的应用

背景

公司的搜索项目中,为了性能,做了大量的内存缓存,这些缓存的更新使用了比较多的timer.

同时,印象中记得有次Go版本升级,使用了四顶堆提升timer性能.

基于这些原因,在复习数据结构-堆的应用时,可以做结合工作生产实践做一些发散.

本文想讲的应用场景: 高性能定时器

假设我们有一个定时器,定时器中维护了很多定时任务,每个任务都设定了一个要触发执行的时间点

主要内容

和技术朋友讨论的一些东西

执行方式

  • gocron有两种,这是我知道的

shell: 在远程主机上执行shell命令 HTTP: 执行HTTP-GET请求

  • 朋友公司使用的方式. 他们公司规模较小,使用比较简单的方式做的

Crontab每隔1分钟扫描任务列表,如果要运行就fork一个进程运行。// 这个任务可以叫创世任务(因为了解区块链的创世区块,所以这里突然想到了这个名字)

附录

1.堆的应用 2.gocron - 定时任务管理系统

从⽤户进程创建socket,到⼀个⽹络包抵达⽹卡到被⽤户进程接收到的总体流程

背景

在分析粘包和半包问题时,对于Go net包下的Client的Read和Write方法的底层原理产生了一些疑问`,

// client.go 通过套接字发送消息
conn.Write([]byte(words))
// server.go 通过套接字读取消息
n, err := conn.Read(buffer)

网络通信中的套接字是什么鬼

主要内容

// 附录1 从⽤户进程创建 socket,到⼀个⽹络包抵达⽹卡到被⽤户进程接收到,总体上的流程图如下: _config.yml

TCP客户/服务器程序所需的基本套接字函数

// 附录2 _config.yml

附录

1.本机: 理解了实现再谈网络性能.pdf-1.3 TCP下用户进程如何和内核协同? 2.本机: UNIX网络编程卷1:套接字联网API(第3版)-第4章 基本TCP套接字编程

Go的http请求成功,响应码200,但是读取body报超时

背景

调用第三方接口时发现的报错

日志中错误:net/http: request canceled (Client.Timeout exceeded while reading body)

demo

fastFailHttpClient := &http.Client{Timeout: time.Second * 2}
resp, httpErr = ad.fastFailHttpClient.Do(req)
    if httpErr != nil || resp == nil || resp.StatusCode == 404 {
       // 建议重试
    }
    defer func() { _ = resp.Body.Close() }()
    break
}
if httpErr != nil || resp == nil {
    //
}
respCode = resp.StatusCode
if respCode == 200 {
    tmpBody, errRead := ioutil.ReadAll(resp.Body)
    if errRead != nil {
        logger.Error("read resp error", zap.String("err", errRead.Error())) //日志中错误:net/http: request canceled (Client.Timeout exceeded while reading body)
        return
    }

原因

// 附录1

1.客户端读取超时:已建立好连接,已经开始返回数据,但是body 太大太慢:

2.源码注释

/usr/local/go/src/net/http/client.go:56

type Client struct {
	// Timeout specifies a time limit for requests made by this
	// Client. The timeout includes connection time, any
	// redirects, and reading the response body. The timer remains
	// running after Get, Head, Post, or Do return and will
	// interrupt reading of the Response.Body. (这里说了,如果超时,会中断do和read body操作)
	//
	// A Timeout of zero means no timeout.
	//
	// The Client cancels requests to the underlying Transport
	// as if the Request's Context ended.
	//
	// For compatibility, the Client will also use the deprecated
	// CancelRequest method on Transport if found. New
	// RoundTripper implementations should use the Request's Context
	// for cancellation instead of implementing CancelRequest.
	Timeout time.Duration
}

处理方案

增加Timeout

附录

1.Go 网络处理中的几种错误– 客户端读取超时:已建立好连接,已经开始返回数据,但是body 太大太慢

Canal+阿里云的RocketMQ

主要内容

1.RocketMQ Name Server的功能与定位。类比微服务架构

2.搭建基于Canal+阿里云的RocketMQ的数据同步环境的一些问题的原因解析

背景

最近在搭建另一套基于Canal+阿里云的RocketMQ的数据同步环境,在canal-server投递到阿里云RocketMQ时,不能投递成功,从canal-instance日志中遇到一些报错:

Caused by: java.lang.RuntimeException: org.apache.rocketmq.client.exception.MQClientException: No route info for this topic, [MQ实例]%[topic名字]

但是我在本地搭建了一个RocketMQ,是可以正常投递的.

TODO 画一些交互图,时序图 来记录动态运行的流程.

Q:为什么需要Name Server这个角色的存在?

A: Producer、Consumer在发送、消费的消息都是与Broker打交到,那它们是怎么知道与哪个Broker去交互呢?

类似这样的信息就需要从Name Server去获取。Name Server提供的最主要的功能就是Broker信息的发现、Topic路由信息的维护。

Name Server承担的就是服务发现、配置分发功能。

A: 简单总结

producer和consumer 都需要 先去 broker(服务注册中心) 拿到对应微服务(broker)的地址, 然后根据这个地址去和对应微服务(broker)进行数据交互.

broker告诉producer标记不同topic的消息应该发往哪个broker。

broker告诉consuer需要去哪个broker获取指定topic的消息。

Q:

Name Server提供的最重要的功能就是Topic路由信息的维护,任何发送消费的消息都归属于某个Topic,发送消费都需要和Broker打交道。Name Server维护了Topic/Broker的关联关系。

Topic/Broker之间的关系则是Broker定期上报Name Server,Name Server自身定期检查Broker信息是否有效,如果Broker宕机或下线,对应的路由信息会被移除。

Name Server中用于维护相关信息的代码实现主要位于RouteInfoManager,其关键用于记录的成员为,

A: 简单总结

  1. broker相当于微服务,name-sever相当于服务注册和发现中心

2.微服务(broker)通过发送心跳包刷新自身的存活状态. 如果微服务(broker)挂了,那么就将这个微服务(broker)从注册中心摘掉。

附录

1.RocketMQ Name Server的功能与定位

RPC框架原理-TCP粘包(应用层协议设计)

背景

研究一下RPC的底层原理的过程,遇到TCP粘包问题(粘包这个词不对,会让人误解)

本文可以学习两个概念来理解何为粘包

Q:首先,什么叫“包”

Q:第二,什么叫“粘”?这里同样有两种含义。

最后可以学到: 粘包是个土话,不专业,专业的说法应该是: stream2datagram OR TCP_NODELAY

附录

怎么解决TCP网络传输「粘包」问题?-首先,什么叫“包”? 在基于tcp开发应用的语境下,其实有两种“包”,其一是tcp在传输的时候封装的报文,分为包头和负载,其二是应用开发者在应用层封装的报文结构。

自己动手写RPC(一)-需求篇

背景

看了一篇自己动手写RPC框架的博客,觉得通过造轮子来学习的方式很好。

预备知识

1.进程间通信的方式 2.为什么

Q:What is Serialization?

Q:什么是序列化

// 附录1 A: 为了把内存中的对象进行网络传输和持久化存储,需要把内存对象转换为字节数组的过程. The string (an in-memory object) abc would be serialized as 0x61 0x62 0x63 (ASCII) or 0x00 0x61 0x00 0x62 0x00 0x63 (UTF16)

Q:为什么要转换为字节数组

A:因为网络传输和持久化存储的方式都是IO,IO的基本单位都是字节数组,所以必须转换为字节数组。

Q:什么是反序列化

A: 把网络字节数组或持久存储中的字节数组,还原为内存中对象的过程.

Q:为什么需要序列化

// 附录2 A:

  1. 序列化是为网络传输和持久化存储,而这两者的方式都是IO,IO支持的数据格式只有字节数组. 所以当要传输或存储内存对象时,都需要把内存对象转换字节数组。

  2. 对象转成字节数组的时候就制定一种规则

立项

假设,现在你有1个进程

What:什么是RPC

Why:为什么需要RPC

你想想,如果没有 RPC 框架,那你要怎么调用另外一台服务器上的接口呢?

主要内容

Go的几种RPC实现

RPC通信过程基本流程: 序列化-编码-解码-反序列化

RPC通信过程代码流程

附录

1.What is Serialization? 2.为什么要序列化? 3.Go net/rpc 包的深度解读和学习 4.这里有一篇,Java系列的RPC框架,是基于Netty搭建网络模块的: 一起写RPC框架(三)RPC网络模块的搭建一 网络传输模型

mysql-sql.DB-SetMaxOpenConns和SetMaxIdleConns

和一位工作近10年的技术群友沟通

最开始是沟通,状态管理的技术方案,他给了我一个博客,是状态机的简单实现。

过了几天,他加我微信,问我用得咋样。

然后谈到了跳槽和团队管理问题,我问他: code review有没有啥经验总结的文章[奸笑],他发给我下面这个文章

一线技术管理者究竟在管什么事?

mysql-sql.DB-SetMaxOpenConns和SetMaxIdleConns,SetConnMaxLifetime

背景

线上一个定时任务,使用gorm框架,读写阿里云RDS,在创建连接和udpate操作中时不时出现这个错误

[mysql] 2021/08/01 18:30:31 packets.go:37: read tcp 172.17.0.3:48356->172.16.0.25:3306: i/o timeout
panic: 定时改价任务报警: invalid connection; invalid connection,update price,pid:6809290

从附录1可以学到

1.基准测试验证不同配置对性能的影响

2.MaxIdleConns应该始终小于或等于MaxOpenConns.

Q:空闲连接是否可以设置得很大 TODO

A:

  • 空闲连接多的好处 理论上因为空闲连接节省了新新连接的时间,有利于提高性能,从这个角度看,空闲连接越多越好。
  • 空闲连接过多的坏处 1.空闲连接占用内存 2.如果一个连接空闲太久,那么它也可能会变得不可用。 因此,将MaxIdleConns设置得太高实际上可能会导致连接变得不可用,并且使用的资源比使用较小的空闲连接池(使用的连接更少,使用频率更高)的情况下要多。 所以只有你很可能马上再次使用这些连接,你才会保持这些连接空闲。

  • 空闲连接设置的准则 配置合适的连接数,使得这些连接可以频繁的被用到.

golang-mysql超时设置

// 附录2

var DefaultConfig = map[string]string{
	"charset":      "utf8",
	"parsetime":    "True",
	"loc":          "Local",
	"timeout":      "15s",
	"readTimeout":  "2s",
	"writeTimeout": "5s",
	"maxIdle":      "10",
	"maxOpenConn":  "100",
	"maxLifetime":  "1",
}

附录

1.[译]配置 sql.DB 获得更好的性能 2.golang中mysql建立连接超时时间timeout 测试

Go-去掉BCE(边界检查),提高运行时的效率

背景

Q13 如何判断 2 个字符串切片(slice) 是相等的?
针对这个中的b = b[:len(a)],使用go 1.15.6编译,已经没有效果了,在len判断的时候就已经做了优化

遍历slice时处理越界的一个小技巧

b = b[:len(a)] // 这种写法是为了优化边界检查从而提升运行时效率的。在go语言中称之为 Bounds Check Elimination,简称为 BCE。

_ = b[:len(a)]

针对这个中的b = b[:len(a)]使用go 1.15.6编译已经没有效果了在len判断的时候就已经做了优化

验证

package main

func StringSliceEqualBCE(a, b []string) bool {
	if len(a) != len(b) {
		return false
	}
	if (a == nil) != (b == nil) {
		return false
	}
	b = b[:len(a)]
	for i, v := range a {
		if v != b[i] {
			return false
		}
	}
	return true
}

func f(s []int) {
	_ = s[2] // 只有这里有边界检查
	_ = s[1]
	_ = s[0]
}

func main() {
	StringSliceEqualBCE([]string{}, []string{})
	f([]int{})
}
(base) Didis-MacBook-Pro-4:BCE-边界检查 didiyu$ go build -gcflags="-d=ssa/check_bce/debug=1" main.go
# command-line-arguments
./main.go:20:7: Found IsInBounds

Q13 如何判断 2 个字符串切片(slice) 是相等的?

从mysql连接池说起,分析golang tcp连接池 TODO

为什么有这篇

主要看了这篇文章里面的一句话:为了解决TCP无限创建与TCP频繁创建销毁带来的资源耗尽、性能下降问题。 MySQL服务器里有专门的TCP连接池限制接数,采用长连接模式复用TCP连接,来解决上述问题。

Q:尝试学习下,建立连接需要消耗哪些资源

//附录1 那么单条 SQL 执行时间是多少呢?我们统计了一段时间的 SQL 执行时间,发现 SQL 的平均执行时间大概是 1ms,也就是说相比于 SQL 的执行,MySQL 建立连接的过程是比较耗时的。

这在请求量小的时候其实影响不大,因为无论是建立连接还是执行 SQL,耗时都是毫秒级别的。

可是请求量上来之后,如果按照原来的方式建立一次连接只执行一条 SQL 的话,1s 只能执行 200 次数据库的查询,而数据库建立连接的时间占了其中 4/5。

Q:明确连接池为什么可以提高效率

A: 那么为什么频繁创建连接会造成响应时间慢呢?来看一个实际的测试。

Q:怎么写连接池,主要注意啥

Golang 连接池的几种实现案例

golang tcp连接池

golang server epoll client 使用连接池 15万+ qps

mysql群,面试问题

Q: Mysql内部会有连接池这种机制吗?

如何设计一个连接池–golang数据库组件database/sql连接池的实现

// 附录2

参考

1.07丨池化技术:如何减少频繁创建数据库连接的性能损耗? 2.golang http服务器连接重用_系统优化连接池技术原理与实现 -如何设计一个连接池 本文例子主要讲解golang数据库组件database/sql连接池的实现

为什么有TIME_WAIT状态 TODO

Linux高性能服务器编程.pdf 3.4.2

结合工作实践理解-代理模式

业务背景

这个服务对应下图中的广告主server的接口一,一般业内叫监测链接,是一种回调链接,用于用户点击广告,媒体平台通过这个接口通知给我们这样的广告主。

_config.yml

这个业务之前使用php写的,因为初创,工期紧,当时编码人员也是首次接触该业务,对业务特点理解不深入等原因。

导致旧系统具有以下特点:

1.旧系统是纯粹的过程式编码方式,每对接一个新的渠道,就要完全新开发一个渠道代码。

2.在阅读旧代码的过程中,可以很明细的意识到所有渠道的业务处理流程都都是相似的N个步骤,不同渠道间的变化点在于:具体的每个步骤细节有差异。

3.整个业务处理的输入是不同, 数据处理的步骤是相似的, 输出都是构成1个Dao结构体,然后把Dao写表。

经过梳理和分析,我们找到一些变与不变的东西:

1.不同渠道业务具有相似的部分:处理步骤的相似,并且不同步骤没有必然的顺序依赖。

2.不同渠道的变化点在于每个步骤的具体细节不同。

3.所有渠道的输出都是相同的:构造1个Dao结构体。

新设计期望达到的效果

便于维护

1.对接新的广告渠道时,尽可能的能复用,只对接每个步骤的细节部分,主体的步骤顺序不变。

2.同事之间交接服务的时候,也更方便同事迅速看出主要设计,get核心业务点, 明确变化点。

针对业务特点进行设计

  1. 通过接口来实现约束相似的步骤

  2. 通过接口方法的不同实现,来应对步骤内部具体细节的变化

这样设计,是从业务出发,而不是为了硬套设计模式。

设计完成之后,发现这个设计基本就是代理模式,于是调整一些命名,显示的体现这个模式在项目中的应用。

本次设计重构的心得

1.是结合业务特点,找到其中不变和变的部分,然后想办法用接口来承接这个不变的部分,用多态来封装变化的部分。

2.不要用设计模式去套业务,生搬硬套的做法不利于提高自身的软件设计能力。偶尔碰对了,也只是运气。逐步推理出的设计会更好,哪怕这个设计没有名字,也是好的设计。

3.适当考虑和同事讨论设计。 一方面,讨论设计是一个让人愉快,也有意思的事情。另一方面,我最开始和leader讨论,在讨论的过程可以逐渐明确变与不变的东西,同时也能保证目标和leader偏差不大。

代理模式的模版

TODO

代理模式在本次重构中的实现

TODO

20210728-彩蛋

非常尴尬,经过和同事讨论,这个模式实际上是模板方法,而不是代理模式。 不过具体叫什么模式不是主要的,重要的是,能够识别出变和不变的地方,并知道如何利用接口,多态等编程元素来提取不变的东西和封装变化的东西

另外整个项目设计还进行的一些改变。

因为分层之后,什么逻辑应该放哪一层,没有梳理得很清楚,导致一些不太好的地方。

这个之后我再梳理一下。// TODO

附录

1.Go设计模式06-代理模式(generate实现类似动态代理)

2.Go 实现常用设计模式(九)代理模式

结合工作实践理解-编程模式:门面模式(Golang)

门面模式

结合工作实践理解-编程模式:修饰器(Golang)

背景

笔者公司和个人背景

我司成立于2016年,前期核心业务主要采用php实现,因为是初创公司且技术人员流动不小,代码质量比较一般。

我司在2020年初抓住了一个互联网风口,业务迎来暴涨,现有系统维护难度越来越大,所以从CEO和CTO层面开始推动代码重构 重写来提升系统可维护性。

我就是这个背景下于2020年4月入职我司。笔者所在技术团队这2年的主要任务是服务化。

入职之后,大致经历了以下阶段:

1.磨合期:现在回想起来,就是和同事互相熟悉性格和工作习惯,便于各自找准角色定位。

2.微框架搭建:调研微服务框架,搭建基础设施(微服务模版,监控,CI/CD,注册中心Consul,Canal,MQ),明确团队的主要职责:基础架构和业务开发,五五开。

3.服务迁移(2020下半年):主要是对周边业务和部分涉及面较小的核心业务进行迁移。用Go对php进行重写。

4.服务重构(2021下半年):对剩余的老大难系统进行重新一定程度的设计和重构。用Go对php进行重写。

围绕这个核心任务,我们团队做了如下事情:

1.搭建微服务开发基本框架. 主要是基于go-micro做一些一些封装,然后在此基础上逐个迁移服务。

2.梳理分析老系统的业务流程和特点,给出相对更好的设计,然后用Go实现,配合业务部门逐渐实现迁移。

在以上工作过程中,我深刻的意思到软件设计的必要性,同时也意识到自己对软件原则和设计模式的应用还比较初级,未能达到得心应手的程度。

为了可以提升自己在应对复杂业务需求时的设计能力,最近在阅读<编程的逻辑-如何用面向对象方法实现复杂业务需求>,同时也在重构部分核心业务, 希望可以结合自身重构经历,再次理解一些常见设计模式的原理,熟悉其应用场景。

装饰器模式原理和解决的问题

参考

1.GO编程模式:修饰器

2.Go 设计模式之装饰器模式

3.<编程的逻辑-如何用面向对象方法实现复杂业务需求>

4.<Head First设计模式-第3章 装饰对象装饰者模式> 本章可以称为"给爱用继承的人一个全新的设计眼界"。 我们即将再度探讨典型的继承滥用问题。你将在本章学到如何使用对象组合的方式,做到在运行时装饰类。 为什么呢? 一旦,你熟悉了装饰的技巧,你将能够在不修改任何底层代码的情况下,给你的(或别人的)对象赋予新的职责。

5.PYTHON修饰器的函数式编程

回顾我见过的Web框架中的参数绑定与校验-TODO

背景

关于web框架 自动绑定和校验 表单参数,在之前的几家公司内部自研web框架都有见过,包括Java和Go的自研框架。

最近使用gin搭建广告服务,也涉及了相关知识点,这里记录一下绑定原理。

Go框架中,主要是依靠解析结构体中的tag进行匹配。

其中主要的难点是:

  • 映射难点

1.Go是静态语言,在判断字段类型时,会有很多分支进行判断。

2.对结构体的指针和非指针赋值逻辑不同。

3.POST请求发送的body如果是嵌套的json内容,映射到对应的嵌套结构中需要使用递归算法,实现一个生产级的夹杂着业务的递归算法还是比较繁杂的。

  • 校验难点 1.字段有无的校验 2.嵌套字段的校验

其他

PS.本文均未谈及性能问题

参考

1.Gin框架系列04:趣谈参数绑定与校验

go-callvis阅读其他项目代码的神奇工具[TODO]

技术讨论群取群友分享的代码阅读的另类工具

参考

1.此工具的目的是使用来自调用图的数据及其与包和类型的关系,为开发人员提供 Go 程序的可视化概览。 这在代码复杂度更高的大型项目中特别有用,或者当您只是试图理解其他人的代码时。

电商商品状态流转的处理方式调研-TODO

背景

我所在的公司是一个二手电商公司,所有即涉及买家,也涉及卖家,所有商品状态流转也就涉及两条主线(买家&卖家),并且因为历史原因,这两条主线还有一定的交织和关联。

其中,买家侧的商品状态比较少,流转情况也较少,出现问题也较少,本篇文章不深入分析,只有和卖家侧商品状态有交织时会提及。

文章主要分析卖家侧的商品状态流转。

卖家侧的商品状态有很多,因为每一个商品处理环节都需要定义状态,甚至还有不少状态有子状态,所有商品管理平台和卖家侧展示,这两块业务,常常出现问题,并且每次解决都需要重新梳理相关状态,导致问题解决特别棘手。

20210826更新-第三次会议讨论

切换的一个新的角度来看待商品状态流转的问题。

从不同的系统角色出发,划分买家端,卖家端和中间处理状态这三种状态.

其中买家端状态不复杂,只需要一级状态即可表示. 卖家端和中间处理状态较为复杂,都才有二级状态进行表示。 所以一共是1+2+2=5个状态表示全部状态,这5个字段主要用于查询和筛选某些状态的商品列表。

这5个状态字段维护的则放在不同的业务逻辑。这样就把状态查询和状态管理分离开来。

简化了查询业务的代码,但是状态管理业务的代码应该是没有很多改进。

本次会议成果要点基本如上。

我个人认为附录2的有限状态机和状态转换表是必须要梳理的东西,但是精力比较有限,想法也没有很成熟,所以暂时没有在会议上提出。

2021-12-11的思考

最近公司多个技术领导,准备用领取驱动来对后台管理系统进行重构。我保持悲观态度。 又想到了这个商品状态流程的问题,再一次调研状态机,发现附录3的文章提到了状态机的一些应用。 同时我也梳理了商品状态流转的主要问题:

  1. 状态回退。
  2. 主状态和子状态。
  3. 梳理异常流程和分支流程。 哪些状态算分支,哪些算主流程,划分的标准是什么? 至于状态梳理的多少,其实不是本质问题,流程多,那么状态自然就多了,这是业务流程决定的,是无法改变的事实。

参考

1.从体验角度看电商前端订单状态流转与后台联动-三、如何绘制订单状态流转

2.关于B端状态流转的思考

2.1深入浅出理解有限状态机

3.大中台模式下如何构建复杂业务核心状态机组件

gin-router代码阅读

背景

今天遇到一个需求

我: gin怎么匹配这种路由: 域名/xxx-notify 和 域名/yyy-notify, 可以提取里面的xxx,yyy

群友A: 把xxx,yyy作为参数

我: url已经约定了,不能改

群友B: 可以获取到xxx-notify,然后自己按-分割出xxx

package main

import (
    "net/http"
    "strings"

    "github.com/gin-gonic/gin"
)

func main() {
    r := gin.Default()
    r.GET("/user/:name/*action", func(c *gin.Context) {
        name := c.Param("name")
        action := c.Param("action")
        //截取/
        action = strings.Trim(action, "/")
        c.String(http.StatusOK, name+" is "+action)
    })
    //默认为监听8080端口
    r.Run(":8000")
}

我: 试了,确实可以,牛皮

package main

import (
	"fmt"
	"net/http"
	"strings"

	"github.com/gin-gonic/gin"
)

func main() {
	r := gin.Default()
	r.GET("/adds/*-notify", func(c *gin.Context) {
		action := c.Param("-notify")
		action = strings.Trim(action, "/")
		ss := strings.Split(action, "-")
		fmt.Println("渠道信息",ss[0])
		c.String(http.StatusOK, "%+v", ss)
	})
	//默认为监听8080端口
	r.Run(":8000")
}

我: c.Param这里是咋匹配出-notify的结果的呢[捂脸]

群友B: 啥意思

我: 没太明白gin的前缀树是咋匹配到/weibo-notify这部分的。

TODO 具体解析

前缀树代码有的难,一时不好看,尤其是通配符:*的处理

参考

1.gin的路由就是基于这个写的 2.路由图解-TODO-通配符和树结构图示

互联网广告的渠道归因

背景

最近接手一个广告业务,包括广告下,广告渠道归因,广告上报,其中主要的难点在广告渠道归因,有很多坑,这里做一些记录。

参考

1.关于互联网广告的渠道归因,你需要知道这些

2.归因分析(Attribution Analysis)模型解析

3.多渠道推广场景下,如何实现App用户增长的精准归因?– HTML5的渠道又无法获取用户的设备号信息,所以这次行为很可能就会被归因在优先级较高的信息流形式下,导致误差的产生

4.广告监测平台(二):详解渠道归因: 1.渠道归因的核心:跟踪链接 2.第一,向应用(APP/H5/WEB)提供链接区分广告渠道 3.归因回溯窗口

5.4个方面解析:归因分析模型

linux常用的命令

1.查看占用端口的进程

方法1

netstat -tunlp | grep 1111

方法2

lsof –i: 端口号

  • 用途
  1. 需要杀死端口时,需要看下这个端口被哪个进程占用的。

方法3

// 附录1 ss -ltnp |grep 端口号

# -l 表示只显示监听套接字
# -t 表示只显示 TCP 套接字
# -n 表示显示数字地址和端口(而不是名字)
# -p 表示显示进程信息

netstat 和 ss 的输出也是类似的,都展示了套接字的状态、接收队列、发送队列、本地地址、远端地址、进程 PID 和进程名称等。

2.查看进程占用了哪些端口

  1. 查找进程 ps -ef | grep tomcat

  2. 根据进程找端口 netstat -nltp | grep 11694

查看谁正在使用某个文件(用户+进程)

// 附录2,3,4

[xxx@pre pre-price-srv]$ lsof pre-price-srv
COMMAND     PID  USER  FD   TYPE DEVICE SIZE/OFF    NODE NAME
pre-price 13569 xxx txt    REG 253,17 83395167 2382348 pre-price-srv
pre-price 13570 xxx txt    REG 253,17 83395167 2382348 pre-price-srv

使用fuser来查文件或目录被谁占用

Linux下根据进程ID查看进程文件的路径

1.执行命令:pwdx  PID 命令,就得到进程jar包所在的位置
2.执行命令:ll /proc/PID/cwd 命令,也能得到进程jar包所在的位置

参考

1.34 | 关于 Linux 网络,你必须知道这些(下) 2.linux查文件被哪些程序占用—fuser和lsof的使用 3.lsof命令使用场景 4.fuser=使用文件或文件结构识别进程 5.Linux下根据进程ID查看进程文件的路径

大表处理思路(单表大于50G)

背景

2021上半年大促发放了大量的优惠券,导致优惠券表数据量暴增,t表半年约新增3亿条记录,参考附录1查看表数据信息。

  • 查看指定数据库各表容量大小。表空间
    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)'
    from information_schema.tables
    where table_schema='xxx'
    order by data_length desc, index_length desc;
    
  • 查询结果如下,t单表大于50GB:数据40G+索引22G
数据库 表名 记录数 数据容量(MB) 索引容量(MB)
xxx t 343112190 39595.23 22068.64
  • t表结构示意如下
    -- 主要存放用户领取和使用优惠券的数据
    CREATE TABLE `t` (
    `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '自动编号',
    `user_id` int(10) unsigned NOT NULL COMMENT '所属用户',
    `voucher_id` int(10) unsigned NOT NULL COMMENT '优惠券',
    `voucher_type` enum('all','category','brand') NOT NULL DEFAULT 'all' COMMENT '优惠券类型',
    `used` tinyint(4) NOT NULL DEFAULT '0' COMMENT 'bool状态,是否已经使用',
    `create_time` int(11) unsigned NOT NULL COMMENT '领取的时间',
    PRIMARY KEY (`id`),
    KEY `user_id` (`user_id`),
    KEY `voucher_id` (`voucher_id`),
    ) ENGINE=InnoDB AUTO_INCREMENT=383790307 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;
    

三个方案

1.分库分表的方案

略 2021-08-11更新: TODO 从Java生态中了解到了一些组件,然后在Go生态中查找了相关资料. 分库分表实践-Sharding-JDBC mysql sharding 官方_GitHub - endink/go-sharding: Mysql 分库分表中间件 小米开源数据库中间件Gaea实战(有彩蛋)

2022-02-08更新:分库分表的步骤

// 附录11 我在公司内部一直奉承的重构思路:步子大了小心扯着蛋.

1.搜集sql,得出分库,分表或分区键 2.如果要水平分表,需要寻找自增id的替代。这种情况可以先去掉数据库中自增ID,为分片和后面的数据迁移工作提前做准备。 // 小步前进 3.双写 4.mysql或polardb-x

2.冷热分离的方案

分库分表会导致多个业务线和统计业务的编码复杂化,所以不考虑分库分表,采用一种类似冷热分离的思路: 使用2个表。

  • 1个热点数据表(阿里云rds),给业务用。单表总量基本可以维持在一亿以内,每几个月清理一次数据,性能尚好。

  • 1个全量表(阿里云adb),给统计用。单表一年新增10亿以内,性能还好。

每n天通过脚本将一段时间内的热点数据表的数据(insert|update|delete),增量的同步到全量表。

3.阿里云的drds

有了方案2,暂时不考虑这个。原理可参考附录2。

最终方案

方案2

效果

待我搞完才知道,应该能支持个3-5年。

经验总结

尽量避免分库分表,采用冷热分离的思路

LOG

发到一些技术交流群中,收到如下反馈

Q:如果要查冷数据,那怎么办?

A(我): 确实有一部分冷数据一直保留在热点表里面的,就是那种只能领取一次的券,需要校验用户只能领取一次。 除此之外,只有统计需要查询其他的冷数据,业务上不需要查。 所以删除的冷数据仅仅指的是,业务上不再需要查询的那部分冷数据,这部分数据是大头。 业务上需要查询的冷数据,则保留在热点表中。

A(我): 估计分库分表我要重新考虑下,在多个群被质疑了

A(其他): 分库分表分场景,优惠券适合冷备吧

A(其他): 哦,有大量的优惠券没有领取

A(我): 恩,所以很多都没意义了,没有领取又失效了,那么业务上就没意义了,只有统计上需要。

Q: 云原生分布式数据库 PolarDB-X,这个方案如何?

A(我): 还在调研,暂未考虑。

Q: 全量表三年以后不是要有50亿,30亿数据还怎么查?

A: 估计分库分表我要重新考虑下,在多个群被质疑了

今年又进行了表数据的清理

因为今年升级了mysql8.0 // 附录3

表碎片清理

附录4,5 查看碎片率

SELECT CONCAT(table_schema, '.', table_name)                   AS  TABLE_NAME
      ,engine                                                  AS  TABLE_ENGINE 
      ,table_type                                              AS  TABLE_TYPE
      ,table_rows                                              AS  TABLE_ROWS
      ,CONCAT(ROUND(data_length  / ( 1024 * 1024), 2), 'M')    AS  TB_DATA_SIZE 
      ,CONCAT(ROUND(index_length / ( 1024 * 1024), 2), 'M')    AS  TB_IDX_SIZE 
      ,CONCAT(ROUND((data_length + index_length ) 
            / ( 1024 * 1024 ), 2), 'M')                        AS  TOTAL_SIZE
      ,CASE WHEN  data_length =0 THEN 0
            ELSE  ROUND(index_length / data_length, 2) END     AS  TB_INDX_RATE
    ,CONCAT(ROUND( data_free / 1024 / 1024,2), 'MB')           AS  TB_DATA_FREE 
    ,CASE WHEN (data_length + index_length) = 0 THEN 0
             ELSE ROUND(data_free/(data_length + index_length),2) 
     END                                                       AS  TB_FRAG_RATE
FROM information_schema.TABLES  
ORDER BY data_free DESC;

分库分表再提

// 附录6 业务增长,数据量增长,准备做一些分库分表的事情

PolarDB-X如何解决不走分表键值对导致全表扫描的问题

// 附录8,9 mysql单索引下二级索引的原理:

分布式二级索引原理:

大表DDL语句的执行

阿里云的DMS,无锁变更执行DDL语句

  1. 加索引。往中间表拷贝数据是用的如下语句:
INSERT IGNORE INTO `库`.`tp_2459945_ogt_t_user_voucher(临时表)` (`xxx`)
SELECT `xxx`
FROM `库`.`表` FORCE INDEX (`PRIMARY`)
WHERE `id` > ?
	AND (`id` < ?
		OR `id` = ?)
LOCK IN SHARE MODE;
  1. 注意选择执行方式。大表写入可能会导致主从延迟。需要选择主从阀值控制执行速率。

附录

1.MySQL查看数据库表容量大小-3.查看指定数据库容量大小

2.首页 > PolarDB-X 云原生分布式数据库 > 技术白皮书 > 技术原理

3.MySQL8.0的information_schema.tables信息不准确怎么办

4.mysql - information_schema.TABLES.DATA_FREE 在 MySQL 中是什么意思?

5.MySQL表碎片化(Table Fragmentation)的原因

6.超详细的mysql分库分表方案

7.淘宝双11达到百亿级数据分表后,怎么实现分页查询?

8.PolarDB-X-全局二级索引.基本原理

9.PolarDB-X什么样的技术,才能让你“忘掉”分区键这个东西呢。-二级索引与分区键

10.PolarDB-X-分库分表子句和参数

11.分库分表带来的问题-我们的系统真的需要分库分表吗?-这种情况可以先去掉数据库中自增ID,为分片和后面的数据迁移工作提前做准备

【线上事故处理】磁盘满了的处理步骤复盘总结

事故简述

一早上来到公司,看到报警有大量报警信息。

云服务器实例ID=xxxx,device=/dev/vda1] /dev/vda1 ,磁盘使用率(100%>=97% )(100%>=97%),持续时间5小时46分钟 

这个服务主要有2个功能 1.作为定时任务的主要执行机器。 2.部署了archery这个sql审核平台。

一开始怀疑是某个定时任务产生了大量日志文件,问了周围同事,最近并没有新增定时任务。

靠经验判断不行,那只能去定位大文件的位置,然后再继续分析问题了。

磁盘满了,定位大文件的通常步骤

  • 1.使用df -h查看磁盘空间占用情况。

可以发现是/dev/vda1磁盘占用较多,对应/根目录

aplum@task:/ df -h
Filesystem                                      Size  Used Avail Use% Mounted on
/dev/vda1                                        99G   93G    2G  95% /
/dev/vdb1                                       985G  322G  618G  35% /data
  • 2.使用 du -s /* | sort -nr 命令查看那个目录占用空间大

因为没有权限,所以我这里给出示意结果。

/data/aplum/Archery-1.7.10/src/docker-compose  占用很大
du -lh –max-depth=1 sort -n
  • 3.然后看哪个目录占用多,再通过 du -s 怀疑的路径 | sort -nr 一层层排查,找到占用文件多的地方。

我今天发现的问题是/data/aplum/Archery-1.7.10/src/docker-compose/mysql/datadir/

  • 4.查看archery的docker-compose.yaml文件,发现mysql容器挂载到了该目录。

archery应该执行某种操作,导致archery的mysql插入了大量的数据.

version: '3'

networks: 略

services:
  redis:略

  mysql:
    image: mysql:5.7
    container_name: mysql
    restart: always
    ports:
      - "3306:3306"
    volumes:
      - "./mysql/my.cnf:/etc/mysql/my.cnf"
      - "./mysql/datadir:/var/lib/mysql"  # mysql容器挂载到了该目录
    environment:
      MYSQL_DATABASE: archery
      MYSQL_ROOT_PASSWORD: xxx
    networks:
      - cow-cow

  inception: 略
  goinception: 略
  archery: 略
  • 5.因为昨天同事通过archery平台提交了一个大表的删除语句,该表一共3000w+的数据,此次删除2000w+,按照id拆分为十多个如下删除语句:
DELETE FROM 某个大表 where user_id = 0 and id BETWEEN 0 and 5000000;
  • 6.结合阿里云的云盘监控可以看到2021-07-08 04:30:00左右,云盘使用空间从75%猛增为100%。 这个sql执行时间为2021-07-08 04:05:00,正好接近磁盘分区(/dev/vda1)的使用空间陡增的时间,

_config.yml

发现到大文件后,不能删除,需要移动到/dev/vdb1这个大磁盘中

因为这是archery的docker集群的一个挂载文件,所以只能把archer整体迁移。

正好这个机器还有1个空间充足的磁盘/dev/vdb1,mv过去即可。

迁移docker-compose部署的archery的一些问题

1.首先,Archery的安装方式设计的比较方便迁移, 解压Archery-1.7.10.tar.gz后,所有东西都在里面了. 尤其是docker-compose.yaml中的volumes挂载都是相对路径,这样我只需要把解压后的目录整体移动,其中的数据也会一起迁移,因为mysql的挂载目录也在里面。

2.实际处理问题的时候,因为对于docker-compose的up,down,start,stop命令不够熟悉,导致没有及时解决问题,这里记录一下。

  • 我们错误的迁移步骤

1.我们执行了stop命令

2.然后执行mv迁移解压后Archery目录到/dev/vdb1

3.然后切入到docker-compose的目录,执行docker start,发现启动不了,【报错信息说原来的挂载路径找不到】

//原因是:stop命令只是停止容器,而我们已经移动了挂载的目录,重写删除容器,重新挂载。

4.所以执行down命令删除容器和挂载关系。

5.然后执行docker-compose up命令,archery就在迁移后成功重启了,且其中mysql的挂载目录也切换到了新的磁盘空间。

aplum@task:/data/aplum docker-compose -h
Define and run multi-container applications with Docker.
Usage:
  docker-compose [-f <arg>...] [options] [COMMAND] [ARGS...]
  docker-compose -h|--help
Options:
Commands:
  down               Stop and remove containers, networks, images, and volumes # 停止和删除容器、网络、卷、镜像,这些内容是通过docker-compose up命令创建的. 默认值删除 容器 网络,可以通过指定 rmi 、volumes参数删除镜像和卷。
  start              Start services # 启动已经存在的服务容器。
  stop               Stop services # 停止已经处于运行状态的容器,但不删除它。通过 docker-compose start 可以再次启动这些容器
  up                 Create and start containers # 它尝试自动完成包括构建镜像,(重新)创建服务,启动服务,并关联服务相关容器的一些列操作。链接的服务都将会被自动启动,除非已经处于运行状态。多数情况下我们可以直接通过该命令来启动一个项目。

总结

  1. linux磁盘已满,查看哪个文件占用多。
  2. 如何知道哪个磁盘增长最多. 一般来说,就是文件最大的那个增长最多,有时也需要结合经验判断。
  3. 对于大文件,要么删除,要么移动。我们对于此次问题,因为我们的大文件是mysql容器挂载的数据文件,所以不能删除,需要移动。

archery删除大表时的回滚机制是怎么做的,为什么会占用大量的磁盘空间?

// 附录5 archery每执行一个sql,就会在备份库里生成对应的sql语句,而且产生的回滚语句都是针对一行的, 所以今天的大表的删除语句产生的回滚sql应是2千多万行,导致磁盘占用从75G变为100G,基本对得上。

附录6:

统计当前文件夹(目录)大小,并按文件大小排序: du -sh * | sort -n du -lh –max-depth=1 |sort -n

2022-02-28更新

// 附录7,8

  • 背景 也是磁盘报警,然后我们发现是日志切割问题导致产生了一个超大的日志文件。 然后我们删除了这个文件,但是df -lh查看的Avail空间并没有减少 和 du -sh *去日志目录统计磁盘占用, 发现并没有减少。 TODO 这里好像可以说明磁盘文件并没有被释放。需要理解磁盘原理后明确这点。

  • 现象 rm删除,但是发现并没有释放磁盘空间。需要重启进程才能真的释放磁盘空间

通过lsof命令查找运行中的进程占用已经删除的文件:

lsof | grep delete |sort -nrk 7|more
  • 原因 因为某个服务的日志切割方法,是很早之前实现,没有经过严格测试。 存在某些情况,某个日志文件无法切割,逐渐变成大文件。 猜测:日志切割流程中存在重命名和删除操作,某个操作会导致进程持有fd。即使导致我们在命令行删除文件时,该文件fd还一般被进程占用,不能真正被删除? 可以使用lsof | grep delete |sort -nrk 7|more查看

参考

1.linux磁盘已满,查看哪个文件占用多 2.goInception Docs-TODO回滚机制 3.docker compose 几个命令 4.docker compose down命令的作用 5.Inception 备份功能说明 6.Linux查看文件或文件夹大小: du命令 7.lsof 命令用法:查看已删除空间却没有释放的进程 8.利用lsof命令查找已经删除的文件来释放磁盘空间

字符串前缀索引

现象

CREATE TABLE `xxx` (
  `id` int(10) NOT NULL AUTO_INCREMENT,
  `token` varchar(1024) NOT NULL COMMENT 'xxx',
  `resp_body` text NOT NULL COMMENT 'xxx',
  `resp_code` int(11) NOT NULL DEFAULT '0' COMMENT 'xxxx',
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  PRIMARY KEY (`id`),
  KEY `token` (`token`), // 长字符串索引
  KEY `resp_code` (`resp_code`),
  KEY `create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='xxx';

执行上面的sql报错

RDS MySQL提示“Error 1709: Index column size too large. The maximum column size is 767 bytes.

处理

CREATE TABLE `xxx` (
  `id` int(10) NOT NULL AUTO_INCREMENT,
  `token` varchar(1024) NOT NULL COMMENT 'xxx',
  `resp_body` text NOT NULL COMMENT 'xxx',
  `resp_code` int(11) NOT NULL DEFAULT '0' COMMENT 'xxxx',
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  PRIMARY KEY (`id`),
  KEY `token` (`token`(10)), -- 字符串前缀索引
  KEY `resp_code` (`resp_code`),
  KEY `create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='xxx';

TODO 原理

为什么需要字符串前缀索引

参考

1.RDS MySQL提示“Error 1709: Index column size too large. The maximum column size is 767 bytes.

文件句柄和文件描述符的区别和理解

https://mp.weixin.qq.com/s/3B1Or4Y-rTRZ0_0shPxhBQ

https://mp.weixin.qq.com/s/s8RxsKJrXuQlRsEQB2GYgw

https://mp.weixin.qq.com/s/3B1Or4Y-rTRZ0_0shPxhBQ

从事故中总结一个:ECS服务器重启后的操作手册

昨天定位OOM的时候,有重启ECS服务器,重启之后,发现需要做一些基础准备工作,主要是几个agent程序。

1.手动重启go-cron的agent  // 定时任务调度框架
命令:
2.【已经添加开启启动项】自动部署需要运维手动重启/home/staragent/bin/staragentctl ,root权限  // 用于阿里云效流水线
3.【已经添加开启启动项】consul的agent需要手动重启 // consul服务注册发现的agent
命令:

从源码分析gorm何时抛出ErrRecordNotFound

GORM何时抛出ErrRecordNotFound错误

// First find first record that match given conditions, order by primary key
func (db *DB) First(dest interface{}, conds ...interface{}) (tx *DB) {
	tx = db.Limit(1).Order(clause.OrderByColumn{
		Column: clause.Column{Table: clause.CurrentTable, Name: clause.PrimaryKey},
	})
	if len(conds) > 0 {
		tx.Statement.AddClause(clause.Where{Exprs: tx.Statement.BuildCondition(conds[0], conds[1:]...)})
	}
	tx.Statement.RaiseErrorOnNotFound = true // 提升ErrorOnNotFound
	tx.Statement.Dest = dest
	tx.callbacks.Query().Execute(tx)
	return
}

// Take return a record that match given conditions, the order will depend on the database implementation
func (db *DB) Take(dest interface{}, conds ...interface{}) (tx *DB) {
	tx = db.Limit(1)
	if len(conds) > 0 {
		tx.Statement.AddClause(clause.Where{Exprs: tx.Statement.BuildCondition(conds[0], conds[1:]...)})
	}
	tx.Statement.RaiseErrorOnNotFound = true // 提升ErrorOnNotFound
	tx.Statement.Dest = dest
	tx.callbacks.Query().Execute(tx)
	return
}

// Last find last record that match given conditions, order by primary key
func (db *DB) Last(dest interface{}, conds ...interface{}) (tx *DB) {
	tx = db.Limit(1).Order(clause.OrderByColumn{
		Column: clause.Column{Table: clause.CurrentTable, Name: clause.PrimaryKey},
		Desc:   true,
	})
	if len(conds) > 0 {
		tx.Statement.AddClause(clause.Where{Exprs: tx.Statement.BuildCondition(conds[0], conds[1:]...)})
	}
	tx.Statement.RaiseErrorOnNotFound = true // 提升ErrorOnNotFound
	tx.Statement.Dest = dest
	tx.callbacks.Query().Execute(tx)
	return
}

// Find find records that match given conditions
func (db *DB) Find(dest interface{}, conds ...interface{}) (tx *DB) {
	tx = db.getInstance()
	if len(conds) > 0 {
		tx.Statement.AddClause(clause.Where{Exprs: tx.Statement.BuildCondition(conds[0], conds[1:]...)})
	}
    // 没有提升ErrorOnNotFound
	tx.Statement.Dest = dest
	tx.callbacks.Query().Execute(tx)
	return
}
-- gorm@v1.21.10/callbacks/callbacks.go:23
func RegisterDefaultCallbacks(db *gorm.DB, config *Config) {
	//...
	queryCallback := db.Callback().Query()
	queryCallback.Register("gorm:query", Query) // 注册Query处理器
	//...

-- gorm@v1.21.10/callbacks/query.go:13
func Query(db *gorm.DB) {
	if db.Error == nil {
		BuildQuerySQL(db)

		if !db.DryRun && db.Error == nil {
			rows, err := db.Statement.ConnPool.QueryContext(db.Statement.Context, db.Statement.SQL.String(), db.Statement.Vars...)
			if err != nil {
				db.AddError(err)
				return
			}
			defer rows.Close()

			gorm.Scan(rows, db, false) // 调用Scan
		}
	}
}

-- gorm@v1.21.10/scan.go:52
func Scan(rows *sql.Rows, db *DB, initialized bool) {
	columns, _ := rows.Columns()
	values := make([]interface{}, len(columns))
	db.RowsAffected = 0
    // ...
	if db.RowsAffected == 0 && db.Statement.RaiseErrorOnNotFound { // 这里,说明只有RaiseErrorOnNotFound=true,才会提升
		db.AddError(ErrRecordNotFound)
	}
}

参考

1.GORM最佳实践之ErrRecordNotFound的坑

公司内部消息队列服务中的一些限流方法

消息队列服务中的一些限流方法

# 1.Golang 标准库限流器 time/rate 使用介绍 2.常用限流策略——漏桶与令牌桶介绍 3.图解+代码|常见限流算法以及限流在单机分布式场景下的思考

prometheus使用错误导致监控指标大量增长导致OOM复盘总结

钉钉群里收到报警信息

一时找不到原因,让运维同事重启了服务器,先解决问题再定位原因。

查看阿里云ECS的监控情况

_config.yml

先怀疑cpu,磁盘问题,同事们回想一下最近的修改,主要是消息中心和接入阿里云的日志服务。

浏览了最近提交的一些服务的代码,没有看出异常。

重启服务器后,使用vmstat、pidstat 等性能工具,观察系统和进程的 CPU 使用率。 // 附录1

# 所有进程的 CPU 统计信息
pidstat -u 1 10

# 显示 IO 统计信息
pidstat -d

# 显示内存统计信息
pidstat -r 10 100 > pid_mem.lgo &

发现消息中心服务的内存占用不断的缓慢增加

查看系统日志,确实有oom-kill的记录

_config.yml

查看服务的supervisor日志

发现同一时间服务收到SIGKILL信号的重启 _config.yml

alloc的pprof文件定位到问题函数

重启服务器和服务后,获取内存的pprof文件 //附录2

_config.yml

查看源码定位到原因是prometheus的metric无限增加,导致内存增加.

指标中包含了uid,这就导致每个用户都需要生成一个metric。

业务代码示意如下

1.gin通过中间件使用prometheus

    r := gin.New()
	// middlewares: first use, first execute
	r.Use(gin.Recovery())
	r.Use(middlewares.PromMiddleware(&middlewares.PromOpts{ // 一个离职的同事写的
		IncludeEndpoint: "/api",
	}))
	r.GET("/metrics", middlewares.PromHandler(promhttp.Handler()))
    // ...
	msgCenterGroup := r.Group("/api/user/:uid/message", e.msgCenterHdl.ExtractUserID) // 一个新同事写的,也是后续导致问题的地方

2.prometheus中间件:PromMiddleware

// PromMiddleware returns a gin.HandlerFunc for exporting some Web metrics
func PromMiddleware(promOpts *PromOpts) gin.HandlerFunc {
	// make sure promOpts is not nil
	if promOpts == nil {
		promOpts = defaultPromOpts
	}
	return func(c *gin.Context) {
		start := time.Now()
		c.Next()
		status := fmt.Sprintf("%d", c.Writer.Status())
		endpoint := c.Request.URL.Path // 问题在这里,指标取了URL,URL如果是"/api/user/:uid/message",那就是每个用户一个监控指标
		method := c.Request.Method
		lvs := []string{status, endpoint, method}
		if !promOpts.checkIncludeLabel(endpoint, promOpts.IncludeEndpoint) {
			return
		}
		reqDuration.WithLabelValues(lvs...).Observe(time.Since(start).Seconds())
	}
}

3.进入三方库代码

// WithLabelValues works as GetMetricWithLabelValues, but panics where
// GetMetricWithLabelValues would have returned an error. Not returning an
// error allows shortcuts like
//     myVec.WithLabelValues("404", "GET").Observe(42.21)
func (v *HistogramVec) WithLabelValues(lvs ...string) Observer {
	h, err := v.GetMetricWithLabelValues(lvs...)
	if err != nil {
		panic(err)
	}
	return h
}

func (v *HistogramVec) GetMetricWithLabelValues(lvs ...string) (Observer, error) {
	metric, err := v.metricVec.getMetricWithLabelValues(lvs...)
	if metric != nil {
		return metric.(Observer), err
	}
	return nil, err
}

func (m *metricVec) getMetricWithLabelValues(lvs ...string) (Metric, error) {
	h, err := m.hashLabelValues(lvs)
	if err != nil {
		return nil, err
	}

	return m.metricMap.getOrCreateMetricWithLabelValues(h, lvs, m.curry), nil
}

// getOrCreateMetricWithLabelValues retrieves the metric by hash and label value
// or creates it and returns the new one.
//
// This function holds the mutex.
func (m *metricMap) getOrCreateMetricWithLabelValues(
	hash uint64, lvs []string, curry []curriedLabelValue,
) Metric {
	m.mtx.RLock()
	metric, ok := m.getMetricWithHashAndLabelValues(hash, lvs, curry)
	m.mtx.RUnlock()
	if ok {
		return metric
	}

	m.mtx.Lock()
	defer m.mtx.Unlock()
	metric, ok = m.getMetricWithHashAndLabelValues(hash, lvs, curry) // TODO 这里还没细看,看函数名就是根据指标的hash查找指标metric
	if !ok {
		inlinedLVs := inlineLabelValues(lvs, curry)
		metric = m.newMetric(inlinedLVs...)
		m.metrics[hash] = append(m.metrics[hash], metricWithLabelValues{values: inlinedLVs, metric: metric}) // 这里不断增加
	}
	return metric
}

TODO,理论上,通过对alloc的pprof文件执行list getOrCreateMetricWithLabelValues命令,可以定位问题函数中哪一行代码的内存分配情况, 但是,因为线上环境只有二进制文件,没有源代码,所以无法查看。对于这个问题,可以把ppror文件下载到本地,然后再使用list命令结合源代码来分析情况。 因为已经找到原因了,我也就没有继续这么搞。

参考

1.每天学习一个命令:pidstat 查看进程消耗资源

2.关于pprof的使用可以参考我的这篇博客

阿里canal是怎么通过zookeeper实现HA机制的?

最近升级公司的canal集群,同时也了解canal集群的基本交互流程和HA的实现方法。

不同server上的instance要求同一时间只能有一个处于running,其他的处于standby状态

为了保证有序性,一份 instance 同一时间只能由一个 canal client 进行get/ack/rollback操作,否则客户端接收无法保证有序。

理解canal的server和instance // 附录2

  • server代表一个canal运行实例,相当于一个jvm

  • instance相当于一个数据队列,即一个mysql实例 (1个server可以对应多个instance)

HA机制设计的大致实现 // 附录5

canal的ha分为两部分,canal server和canal client分别有对应的ha实现

线上查看zookeeper目录树

[zk: localhost:2181(CONNECTED) 30] ls /otter/canal
[cluster, destinations]
[zk: localhost:2181(CONNECTED) 31] ls /otter/canal/cluster // 表示集群???
[192.168.0.64:11111]
[zk: localhost:2181(CONNECTED) 32] ls /otter/canal/destinations   //这下面表示目前的intances实例
[dev01db, dev02db, dev022db]

TODO canal配置文件和zk里面数据的对应关系

  • canal.properties
    ```bash

    tcp bind ip

    canal.ip =

    register ip to zookeeper

    canal.register.ip = canal.port = 11111 #canal server提供socket tcp服务的端口 #canal-server接收zk通知的端口吗? 这个端口谁来访问? canal.metrics.pull.port = 11112 #c-s暴露的监控数据抓取接口. 可以配置到prometheus.yml的targets里面

    canal instance user/passwd

    canal.user = canal canal.passwd = E3619321C1A937C46A0D8BD1DAC39F93B27D4458

canal.zkServers = 192.168.0.64:2181 ```

  • zk: ls /otter/canal/destinations

  • zk: get /otter/canal/destinations

参考

1.图解阿里canal是怎么通过zookeeper实现HA机制的? 2.理解canal的server和instance 3.Canl实战HA配置 4.「从零单排canal 02」canal集群版 + admin控制台 最新搭建姿势(基于1.1.4版本) 5.canal工作原理及简单案例演示-2.3.7 HA机制设计

【转载】go1.14升级到go1.15,sync.Map出现了内存泄漏

最近团队内部准备升级基础镜像,从go1.13 升级到 go1.16,其中一个同事提到了这个博客,发现我以前也看过这个问题,再次记录以下

踩了 Golang sync.Map 的一个坑

https://github.com/golang/go/issues/40999

【转载】ASA实战分享:详解苹果竞价广告精细化投放策略

ASA实战分享:详解苹果竞价广告精细化投放策略

关于新同事接手新项目的一点思考

从内聚性的角度思考为什么项目代码中要有某些对象,这些对象为什么要有这些属性和方法

Q:从内聚性的角度思考为什么要有ProductInfo对象,这个对象应该有哪些属性和行为(方法),为什么这些属性和方法应该放在这一个类中? // 对象+交互

r, err = (&models.ProductInfo{
        ProductId: product_id,
        UserId:       user_id,
        TmNow:        tm_now,
        IsFirstOrder: firstOrder}).
        GetProductInfo(models.DetailFormat)

golang map和sync.Map两种到底怎么选,谁的性能更加的好?

读多写少

写多

为什么某些常见慢

两种到底怎么选,谁的性能更加的好

阿里开源MySQL中间件Canal快速入门

Canal原理

Canal设计理念 // 附录3

而 Canal 的设计初衷就是为了解决上述问题,故我们可以边思考这个场景,来反推一下 Canal Instance 的设计理念。

Canal架构-server和instance的关系

https://zhuanlan.zhihu.com/p/345736518

  • server 代表一个 canal 运行实例,对应于一个 jvm
  • instance 对应于一个数据队列 (1个 canal server 对应 1..n 个 instance ) // 1个instance对应1个slave //附录3
  • instance 下的子模块

正经的master/slave和伪装的slave

Canal配置文件:canal.properties和instance.properties // 附录5

  1. canal.properties存放instance.properties的共享参数

比如可以将instance.properties的公用参数,抽取放置到这里,这样每个instance启动的时候就可以共享.

  1. instance.properties配置定义优先级高于canal.properties

Canal生产环境问题

最近在搭建一套Canal同步环境,又遇到了一些问题,这里参考附录6{Canal v1.1.4版本避坑指南}做一些记录.

Q: 现象: Canal运行一段时间就会出现,增量数据不同步的问题.

报错: Caused by: com.alibaba.otter.canal.parse.exception.CanalParseException: column size is not match for table:数据库名称.表名称,新表结构的字段数量 vs 缓存表结构的字段数量;

原因: 表结构缓存

Q: intances消费日志出现: No route info for this topic, [MQ实例]%[topic名字]

Caused by: java.lang.RuntimeException: org.apache.rocketmq.client.exception.MQClientException: No route info for this topic, xxxxx%yyyyyy
Caused by: java.lang.RuntimeException: org.apache.rocketmq.client.exception.MQClientException: No route info for this topic, [MQ实例]%[topic名字]

A:原因 未配置阿里云RocketMQ的访问密钥 ak/sk

# aliyun ak/sk , support rds/mq
#canal.aliyun.accessKey =
#canal.aliyun.secretKey =

当时用这个错误在canal的项目里面找到了这个issuev1.1.5 连接阿里云RocketMQ失败 #3064

但是当时不理解ak/sk简写的具体含义,也就错过了这个解决方案。

后来,通过不断的对比配置,缩小配置错误的范围,最终找到了原因,配置之后就OK了.

我也给这个issue提交了comment,说明了ak/sk的意思,希望能帮助后来者.

Q:控制canal同步的起始位点: 更新zk中消息的位点

一、控制同步起始位置

  • 需求背景: 我希望监控我启动

预发布环境RocketMQ下,canal在A1时间点启动,然后A2时间点停止;

经过一段时间后,B1时间点再次启动canal服务:此时canal会先同步A2到B1时间段之间的数据,

但是希望直接从B1(2020-10-14 14:00:00)时间点开始同步数据。

  • 重置位点方案
    1. 停止instance
  1. 删除zk中的位点信息 rmr /otter/canal/destinations/dev02db

  2. 启动instance

2021-11-10 更新

对canal有了一定的线上维护经验,准备做一次分享,所以进一步学习一下架构设计。

Q:1个数据库实例,监听多个表,使用1个instance。那么通过新增instance(每个表1个instance)可以提升canal的消费能力吗?

A:

// 附录7

参考

1.阿里开源MySQL中间件Canal快速入门

2.开源实战 | Canal生产环境常见问题总结与分析 TODO 3.一文详解 Canal Instance 设计理念与定制开发思路-Instance 是 Canal 数据同步的核心,在一个 Canal 实例中只有启动 Instace,才能实现数据的同步

4.Mysql (master/slave)主从复制原理及配置

5.阿里Canal配置内容详解

6.Canal v1.1.4版本避坑指南

7.数据同步中间件canal架构设计

常考O(NLogN)的排序算法总结-快速排序和堆排序

分治思想的运用

  1. 分:整体问题变为局部问题。
  2. 合:然后利用局部问题的解,得到整体问题的解。

有的问题在分阶段起关键作用,有的问题在合阶段起关键作用(比如归并排序,分阶段并没有得到解决,合并阶段完成才得到排序结果)

一些比较形象的分治思想的应用。

分治思想,对应的编程实现方式一般是递归的。

排序问题和分治思想

排序的分治思路:每次处理后,一个或多个树被放在了最终的位置.(排序后的应该在的位置)。或者一个或多个数的相对位置被调整到了和最终的相对位置一致。//如果所有数的相对位置一致,那么这个数组自然就有序了。

递归的编码方式:树形处理顺序,压栈,压栈…,弹栈,弹栈, // 分 [] | A类问题 [] / \ 问题规模更小的A类问题 [] /\ /\ 问题规模再小的A类问题 // 治: 有时合就是治的一种方法.

快速排序: 每次把一个数放在合适的位置,在分的阶段解决问题?

归并排序: 每次把多个数的相对位置调整正确,在合的阶段解决问题? 注意合并之后这些数也没有在最终应该在的位置,只是相对位置已经是最终的相对位置了。

快速排序partition函数扩展

O(n) 时间复杂度内求无序数组中的第 K 大元素。

归并排序总结

归并排序是否稳定排序? 归并排序的空间复杂度是多少? 归并排序为什么使用不广泛,有啥缺陷?

三个O(N2)的排序算法总结

表格对比

O(n2)的排序算法 思路要点   为什么稳定或者不稳定
冒泡排序 每次冒泡把最大的数未排序数组的最后 unsorted+sorted(max) 都是相邻交换,没有跨越式交换,发现相等时可以不移动
插入排序 反向遍历前面的排序部分,腾出位置,插入第一个未排序元素 sorted+unsorted 插入时可以插入相同元素的后面
选择排序 选择未排序区间最小的元素,交换到已排序区间的末尾 sorted+unsorted 交换时可能跨过和自己相等的数,破坏稳定性
  • 一个小总结

可以看出,冒泡排序和选择排序,都是把数组分为未排序和排序的两部分,然后从未排序部分找出最值放入到已排序部分。

  • 思考 特定算法是依赖特定的数据结构的。这三个排序是基于数组实现的,如果基于链表来实现,还能work吗,复杂度又咋样?

系统性的总结mysql在工作中常用的知识(mysql领域分层图和细节分层图)

计划如下:

1.首先了解数据结构 1.1 为什么选择B+tree. 从BST,B-tree,B+tee的演进,同时讨论为什么不选跳表,LSM

2.死锁分析之insert

3.幻读和间隙锁

工作实践-同1台开发机器,不同账户(同事)使用统一版本的thrfit和grpc编译工具的方法

  1. 查找thrift文件位置 /opt/xxxx/thrift/bin/thrift

  2. 新同事,修改.bashrc文件, 把/opt/xxxx/thrift/bin/thrift加入到环境变量中

  3. 注意设置完环境变量,需要使用source ~/.bashrc刷新一下使其生效

[xxxx@dev02 ~]$ whereis thrift
thrift: /opt/xxxx/thrift/bin/thrift
[xxxx@dev02 ~]$ echo $PATH
/home/xxxx/.gvm/bin:/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/xxxx/.local/bin:/home/xxxx/bin:/opt/xxxx/go1.13/bin:/opt/xxxx/thrift/bin

参考

linux 查看环境变量和修改环境变量

工作中遇到的问题-如何避免长事务对业务的影响?

其次,从数据库端来看: 【TODO】都值得试一试

1.监控 information_schema.Innodb_trx 表,设置长事务阈值,超过就报警 / 或者 kill;

2.Percona 的 pt-kill 这个工具不错,推荐使用; // 附录2,3

pt-kill字面意思就是: kill掉MySQL满足某些特征的query语句

3.在业务功能测试阶段要求输出所有的 general_log,分析日志行为提前发现问题;【TODO】工作中试下

4.如果使用的是 MySQL 5.6 或者更新版本,把 innodb_undo_tablespaces 设置成 2(或更大的值)。 如果真的出现大事务导致回滚段过大,这样设置后清理起来更方便。 // 附录5 【TODO】

参考

1.如何避免长事务对业务的影响?

2.pt-tools系列:pt-kill 官方使用文档

3.Percona-Toolkit系列之pt-kill杀会话利器

4.general_log

5.innodb_undo_tablespaces的含义。undolog表空间清理 执行一个长事务,可以肉眼观察到undolog日志文件的膨胀。

insert死锁场景分析 1. duplicate key error引发的死锁

参考

mysql insert锁机制(insert死锁)

1.insert操作会加X锁

Q:为什么insert要加X锁 A:这里的x锁是行锁,如果不加,那岂不是能插入两条id为6的值

2.insert操作如果发生重复唯一键冲突,则不会加X锁,而是加S锁

这里理解错误了: select * from information_schema.innodb_locks;里面获取的是准备获取但是还没有获取到的哪些锁.

_config.yml

  1. X锁和S锁是兼容的吗?
T1(36727) T2(36728) T3(36729)
begin; begin; begin;
insert into aa values(6, ‘test’, 12, 3);    
  insert into aa values(6, ‘test’, 12, 3);(等待t1释放X锁,等待加S锁)  
    insert into aa values(6, ‘test’, 12, 3);(等待t1释放X锁,等待加S锁)
rollback;    
    ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
  Query OK, 1 row affected (13.10 sec)  

MySQL · 引擎特性 · InnoDB隐式锁功能解析

只有在特殊情况下,才会将隐式锁转换为显示锁。
这个转换动作并不是加隐式锁的线程自发去做的,而是其他存在行数据冲突的线程去做的。
例如事务1插入记录且未提交,此时事务2尝试对该记录加锁,那么事务2必须先判断记录上保存的事务id是否活跃,如果活跃则帮助事务1建立一个锁对象,而事务2自身进入等待事务1的状态,可以参考如下例子:

INNODB_LOCKS 和 data_locks

  • 表的功能 The INNODB_LOCKS table contains information about each lock that an InnoDB transaction has requested but not yet acquired, and each lock that a transaction holds that is blocking another transaction.

INNODB_LOCKS表主要包含了InnoDB事务锁的具体情况,包括事务正在申请加的锁和导致其他事务阻塞的锁。 不是已经获取到的锁。

  • INNODB_LOCKS 被 data_locks替代 This(INNODB_LOCKS) table is deprecated and is removed as of MySQL 8.0.1. Use the Performance Schema data_locks table instead. See Section 27.12.13.1, “The data_locks Table”.

看着有点迷糊,自己动手试试

CREATE TABLE aa ( id int( 10) unsigned NOT NULL COMMENT ‘主键’, name varchar( 20) NOT NULL DEFAULT ‘’ COMMENT ‘姓名’, age int( 11) NOT NULL DEFAULT ‘0’ COMMENT ‘年龄’, stage int( 11) NOT NULL DEFAULT ‘0’ COMMENT ‘关卡数’, PRIMARY KEY ( id), UNIQUE KEY udx_name ( name), KEY idx_stage ( stage) ) ENGINE= InnoDB DEFAULT CHARSET=utf8;

字符串拼接性能及原理

字符串拼接性能及原理

阿里云RocketMQ订阅关系不一致导致丢消息问题记录

问题

用户服务的线上环境和预发布环境对RocketMQ的使用,没有遵循订阅关系一致性,导致丢消息。// 参考附录

线上: GID_a&topic=EVENT_USER&tag=AD_SOURCE

预发布: GID_aE&topic=EVENT_USER&tag=AD_SOURCE_PRE

原因

消息队列RocketMQ版里的一个消费者Group ID代表一个Consumer实例群组。对于大多数分布式应用来说,一个消费者Group ID下通常会挂载多个Consumer实例。
由于消息队列RocketMQ版的订阅关系主要由Topic+Tag共同组成,因此,保持订阅关系一致意味着同一个消费者Group ID下所有的实例需在以下两方面均保持一致:
1.订阅的Topic必须一致
2.订阅的Topic中的Tag必须一致(包括Tag的数量和Tag的顺序)

参考

订阅关系一致文档

【转载】i/o timeout,希望你不要踩到这个net/http包的坑:Go内存泄露

这篇博客从常见的i/o timeout现象入手, 经过抓包确认了是自连接创建3s后,http客户端主动断开连接(怀疑超时原因), 然后进一步跟踪源码,同时按照调用链路贴出源码,追踪出了超时原因和超时错误产生的位置.

整个过程环环相扣,读完印象深刻,好像自己分析定位出来的原因一样.

从这篇文章可以学会如下技能:

  • 1.抓包

  • 2.golang net/http设置超时分为两种
    • 2.1 自连接创建时开始计算的超时,如果这个连接被复用,那么每次请求时间都会累计到超时时间里。
    • 2.2 每次请求的超时
  • 3.net/http包复用长连接的缘由

  • 4.net/http包复用长连接的实现(源码分析)
    • 4.1 一个域名,一个长连接,每个连接对应2个携程(1个读,1个写) 所以,如果有N个域名,会有1+2*N个携程,1个建立连接的携程,N对读写携程 // 附录2,携程泄漏
    • 4.2 超时的触发过程
    • 4.3 i/o timeout在源码中产生的位置
  • 5.学习博客中如何贴源代码 按照调用链路贴出函数和关键代码行即可,力求让读者不用看源码,只用看博客的跟踪链路即可明白源码的链路.

参考

1.i/o timeout , 希望你不要踩到这个net/http包的坑

2.携程泄漏分析

3.给大家丢脸了,用了三年golang,我还是没答对这道内存泄漏题

这篇文章后面有很多面试题链接

连nil切片和空切片一不一样都不清楚?那BAT面试官只好让你回去等通知了。
昨天那个在for循环里append元素的同事,今天还在么?
golang面试官:for select时,如果通道已经关闭会怎么样?如果只有一个case呢?
golang面试官:for select时,如果通道已经关闭会怎么样?如果只有一个case呢?
golang面试题:对已经关闭的的chan进行读写,会怎么样?为什么?
golang面试题:对未初始化的的chan进行读写,会怎么样?为什么?
golang 面试题:reflect(反射包)如何获取字段 tag?为什么 json 包不能导出私有变量的 tag?
golang面试题:json包变量不加tag会怎么样?
golang面试题:怎么避免内存逃逸?
golang面试题:简单聊聊内存逃逸?
golang面试题:字符串转成byte数组,会发生内存拷贝吗?
golang面试题:翻转含有中文、数字、英文字母的字符串
golang面试题:拷贝大切片一定比小切片代价大吗?
golang面试题:能说说uintptr和unsafe.Pointer的区别吗?

4.TCP协议之RST与FIN 缓冲区所有数据包都发送完成才会发送FIN包

发送RST包关闭连接时,不必等缓冲区的包都发出去,直接就丢弃缓存区的包发送RST包。而接收端收到RST包后,也不必发送ACK包来确认。

给1个sql,问如何加索引

Q: select count(1) form table where a = 1 and (b=2 or c = 3) and d = 4; 大佬们,这个sql应该咋加索引

A: a和d 联合索引

看区分度吧 a和d如果区分度都高的话,就加联合索引,如果某个值区分度不高就不加

a 只有1和2 d只有1

那还加毛

搜索服务2年的优化之路总结梳理

我在公司内部进行的分享:2021 618大促优化记录.pdf

20210730-更新

今天看到一篇性能优化博客(附录1),其中有几点可以参考

1.辅助工具的使用 2.编写benchmark单独验证。//我之前性能优化只看pprof的结果和最终监控的响应时间,验证得还不够充分。 3.string转[]byte的黑科技 4.需要对redis原理有一定了解,才能知道redis hash需要取多次slot。// TODO 准备学习一些redis数据结构原理

附录

1.一个95分位延迟要求5ms的场景,如何做性能优化

阿里云RDS(mysql)慢sql-自动化报警脚本

背景

公司的mysql数据库是购买的阿里云RDS,1主4从,只有1个主库(读写),其中1个从库只用于统计业务,部分业务只需要读取从库。 比如我负责的搜索业务只需读从库.

问题

各个业务先共享这1个主库,会导致以下问题: 1.主库或从库如果被慢sql把cpu打满100%,会影响公司的所有主库操作。 比如APP首页无法打开,出现乱码等严重问题。 2.慢sql不好定位. 因为我们公司的慢sql相对较多,主要后台管理有相当多的复杂sql(几百行),这样平时就有一定数量的慢sql了. 于是最近新写的慢sql和既有的慢sql混杂在一起,不是很好排查。 目前遇到慢sql打满cpu导致首页不能访问时,我们都是由技术总监在阿里云后台不停的杀死目前执行的全部慢sql,直到首页恢复访问才停止。

这样,基本上从发现到恢复都要10多分钟,不仅吓人,也劳心劳力. 经过搜索一些资料,考虑讲刚刚的过程进行一定脚本自动化。

自动化脚本思路

information_schema.PROCESSLIST查询出执行时间超过10s的sql,然后发送钉钉报警,这样加快慢sql的定位速度.

SELECT * FROM `information_schema`.`PROCESSLIST` Where COMMAND = 'Query' AND Time >= 10;

后续改进

1.这个脚本只是发现sql,并没有自动kill掉这个慢sql。

参考

1.MySQL之查询截取分析(慢查询日志等 ) 2.MySQL 锁表后快速解决方法 及 MySQL中的锁

golang性能优化和gc

_config.yml

一般而言只有升级go版本才能提升gc的能力。

同一个go版本,垃圾更少,gc次数更少, 提高的是吞吐量,而不是gc的能力,垃圾收集器清理垃圾的能力并没有提高。

比如: 10w次请求,产生垃圾w。优化后。20w次请求,才产生垃圾w。

参考

区分 提升gc能力 和 通过减少分配的对象进而减少gc次数 是两种不同的思路

es查询时分词和索引时分词

这个博客讲了例子 有个官方文档提了查询时分词和索引时分词的优劣

--inuse/alloc_space --inuse/alloc_objects区别

通常情况下 用–inuse_space来分析程序常驻内存的占用情况; 用–alloc_objects来分析内存的临时分配情况,可以提高程序的运行速度。

命令

弹出url http://localhost:8082/ui/?si=inuse_space http://localhost:8082/ui/?si=alloc_objects

定位过程

go tool pprof –alloc_objects pprof.product_server.alloc_objects.alloc_space.inuse_objects.inuse_space.001.pb.gz top list 比较大的函数名 查看该函数哪一行分配内存比较多。

需要频繁分配内存的情况

1.log日志打印。

aplum_product/src/product_server/models/db.(*TProduct).SetActivityRule
/Users/didiyu/Downloads/go/src/aplum_product_clear_micro/src/product_server/models/db/set_discount_product.go
  Total:     8490569   22018638 (flat, cum)  5.25%
    179            .          .           func (p *TProduct) SetActivityRule(curDiscount float64, whitelist map[string]bool) { 
    180            .          .           ConfigLoop: 
    181            .          .           	for i, config := range p.ActivityConfigs { 
    182      5996623    5996623                 log.Debug("%v conf %v,%v", i, config.ID, config.Name) 

2.split方法

参考

1.–inuse/alloc_space –inuse/alloc_objects区别

令人激动的一次优化

我负责的搜索服务,每次大促前都会进行一些性能优化. 今天开启mysql的PreparedStatement,p95直接从200ms提升到了100ms.吓死我了。

性能优化的惊喜-PreparedStatement的原理

启动了PreparedStatement之后,其他服务出现如下报警

我擦,然后就报错了。 Can’t create more than max_prepared_stmt_count statements (current value: 16382) 我可以把max_prepared_stmt_count 加满吗

"error":"Error 1461: Can't create more than max_prepared_stmt_count statements (current value: 16382)"

金柱大哥解释

设置的大些,确认下没有拼sql,记得用最近的版本,之前用map的话没排序会产生几条 默认事务关掉性能应该还可以提升

_config.yml

TODO

preparestatement是基于session做存储,如果session读了,就会有较多的重复preparestatement

抓包分析preparestatement

mysql抓包能看到先发包过去编译sql

按照附录3查看performance_schema.prepared_statements_instances,发现没有该库,原因:

云数据库RDS的MySQL版无法查询performance_schema值

查看max_prepared_stmt_count的使用量是否正常

//附录1.1 请确认Com_stmt_close的值是否接近于Com_stmt_prepare

命令: show global status like ‘com_stmt%’; 结果: Com_stmt_close 31552809 Com_stmt_execute 31552885 Com_stmt_fetch 0 Com_stmt_prepare 31552938 Com_stmt_reprepare 0 Com_stmt_reset 0 Com_stmt_send_long_data 0

2021-01-11更新

今天升级mysql8。依然出现了如下错误:

"error":"Error 1461: Can't create more than max_prepared_stmt_count statements (current value: 16382)"

查看阿里云后台主库配置是49146,从库配置还是16382。

问了下运维:只改了主库,忘了改从库了。

设置从库参数即可。

// 附录4

Com_stmt_prepare 减去 Com_stmt_close 大于 max_prepared_stmt_count 就会出现这种错误。那么我们手动调高max_prepared_stmt_count(取值范围:0 - 1048576,默认16382)即可解决

参考

1.database/sql: Stmt的使用以及坑

1.1请确认Com_stmt_close的值是否接近于Com_stmt_prepare – 一个可能的原因是应用端那边没有关闭prepared的语句

2.PreparedStatement的原理

MySQL · 特性分析 · MySQL的预编译功能 TODO

3.mysql_stmt含义_Mysql–由prepared sql statement引发的问题

4.max_prepared_stmt_count 问题与Sysbench 工具简介

redis scan

返回的结果可能会有重复,需要客户端去重复,这点非常重要;

那我们如何去遍历大数据量呢?我们可以采用redis的另一个命令scan。我们看一下scan的特点

复杂度虽然也是 O(n),但是它是通过游标分步进行的,不会阻塞线程

提供 count 参数,不是结果数量,是redis单次遍历字典槽位数量(约等于)

同 keys 一样,它也提供模式匹配功能;

服务器不需要为游标保存状态,游标的唯一状态就是 scan 返回给客户端的游标整数;

返回的结果可能会有重复,需要客户端去重复,这点非常重要;

单次返回的结果是空的并不意味着遍历结束,而要看返回的游标值是否为零

参考

1.返回的结果可能会有重复,需要客户端去重复,这点非常重要;

记一次goroutine内存泄露

参考

1.记一次goroutine内存泄露

Go-micro异常捕获

参考

1.Go-micro异常捕获

Code Review - 如何对slice赋值|slice深拷贝,浅拷贝

type AttributeValue struct {
	attr       []*proto_product.Attribute // slice被直接赋值
	expireTime int64
}

func (cache *AttributeCache) BatchGetCache(pids []int32) (map[int32][]*proto_product.Attribute, []int32) {
	cache.attributeMutex.RLock()
	defer cache.attributeMutex.RUnlock()
	missIds := []int32{}
	attrList := map[int32][]*proto_product.Attribute{}
	for _, pid := range  pids {
		if attr, ok := cache.attributes[pid]; ok {
			attrList[pid] = attr.attr // slice直接赋值,那么修改attrList[pid]本质上会修改cache.attributes[pid]
		} else {
			missIds = append(missIds, pid)
		}
	}

	return attrList, missIds
}

参考

slice作为参数传递,在函数中进行修改,函数返回后,修改能否保留?

Another option is to use slices. Even though your function gets a copy of the slice variable it still references the original data. 即使函数内部得到的是 slice 的值拷贝,但依旧会更新 slice 的原始数据(底层 array)

func main() {  
    x := []int{1,2,3}

    func(arr []int) {
        arr[0] = 7
        fmt.Println(arr) //prints [7 2 3]
    }(x)

    fmt.Println(x) //prints [7 2 3]
}

3.1 大量内存得不到释放 TODO

大量内存得不到释放

在已有切片的基础上进行切片,不会创建新的底层数组。因为原来的底层数组没有发生变化,内存会一直占用,直到没有变量引用该数组。 因此很可能出现这么一种情况,原切片由大量的元素构成,但是我们在原切片的基础上切片,虽然只使用了很小一段,但底层数组在内存中仍然占据了大量空间,得不到释放。 比较推荐的做法,使用 copy 替代 re-slice。

参考

1.Golang 新手可能会踩的 50 个坑–12. Array|slice的值作为函数参数

大数据量sql分页查询很慢,有哪些优化方案? TODO

为什么慢

针对查询偏移量的测试:

select * from orders_history where type=8 limit 100,100;
select * from orders_history where type=8 limit 1000,100;
select * from orders_history where type=8 limit 10000,100;
select * from orders_history where type=8 limit 100000,100;
select * from orders_history where type=8 limit 1000000,100;

三次查询时间如下:

查询100偏移:25ms 24ms 24ms 查询1000偏移:78ms 76ms 77ms 查询10000偏移:3092ms 3212ms 3128ms 查询100000偏移:3878ms 3812ms 3798ms 查询1000000偏移:14608ms 14062ms 14700ms

随着查询偏移的增大,尤其查询偏移大于10万以后,查询时间急剧增加。

这种分页查询方式会从数据库第一条记录开始扫描,所以越往后,查询速度越慢,而且查询的数据越多,也会拖慢总查询速度。

如何改进

子查询

参考

1.大数据量sql分页查询很慢,有哪些优化方案? 2.分页场景(limit,offset)为什么会慢

存储进阶—怎么才能保证 IO 数据的安全? TODO

主要解答的问题

1.怎么才算真的写入,宕机也不会导致数据丢失的那种。 2.数据库系统,是否使用了缓存IO? 3.三种安全的IO方法

参考

1.存储进阶—怎么才能保证 IO 数据的安全?

go map相关操作

主要疑问

2.delete全部key可以清空整个map吗?

A:内存没有释放。清空只是修改了一个标记,底层内存还是被占用了

3.如何真正释放内存?

A: map = nil 这之后坐等垃圾回收器回收就好了。

3.1 验证是否释放内存

var intMap map[int]int
var cnt = 8192

func main() {
	printMemStats()

	initMap()
	runtime.GC()
	printMemStats()

	log.Println(len(intMap))
	for i := 0; i < cnt; i++ {
		delete(intMap, i)
	}
	log.Println(len(intMap))

	runtime.GC()
	printMemStats()

	intMap = nil
	runtime.GC()
	printMemStats()
}

func initMap() {
	intMap = make(map[int]int, cnt)

	for i := 0; i < cnt; i++ {
		intMap[i] = i
	}
}

func printMemStats() {
	var m runtime.MemStats
	runtime.ReadMemStats(&m)
	log.Printf("Alloc = %v TotalAlloc = %v Sys = %v NumGC = %v\n", m.Alloc/1024, m.TotalAlloc/1024, m.Sys/1024, m.NumGC)
}
//Output
2021/05/26 20:40:31 Alloc = 114 TotalAlloc = 114 Sys = 68290 NumGC = 0
2021/05/26 20:40:31 Alloc = 421 TotalAlloc = 435 Sys = 70016 NumGC = 1
2021/05/26 20:40:31 8192
2021/05/26 20:40:31 0
2021/05/26 20:40:31 Alloc = 421 TotalAlloc = 437 Sys = 70080 NumGC = 2
2021/05/26 20:40:31 Alloc = 109 TotalAlloc = 439 Sys = 70080 NumGC = 3

3.2 用map做缓存存在内存不释放的问题

如果你用map做缓存,而每次更新只是部分更新,更新的 key 如果偏差比较大,有可能会有内存逐渐增长而不释放的问题。要注意。 或者map

4.为什么这么设计:删除时只标记不释放内存,只有置为nil才会释放内存.

我们可以在遍历map的时候删除里面的元素,而且可以删除没有遍历到的元素,为了保证删除了之后遍历不发生异常,才这么设计的吧。

并且删除之后,range不会再访问到删除的元素 比如,我有k1,k2,假设依次遍历

func main() {
	m := make(map[int]int)
	m[1] = 1
	m[2] = 2
	for k := range m {
		fmt.Println(len(m))
		delete(m, k)
		delete(m, 2)
		fmt.Println(k, len(m))
	}
}
//Output
2
1 0

5.map的写入如何覆盖bucket里面的entry?TODO

参考

1.Golang map 如何进行删除操作?

go-zeor组件 TODO

参考

1.go-zeor组件

Go 译文之竞态检测器 race

TODO

深入理解panic and recover

1.如果协程A发生了panic,协程B是否会因为协程A的panic而挂掉?

2.如果协程A发生了panic,协程B是否能用recover捕获到协程A的panic?

https://segmentfault.com/a/1190000023691221

了解了以上事实,容易误判下列例子

func main() {
 runtime.GOMAXPROCS(1)
 go func() {
  fmt.Println("2")
  panic("hello goroutine")
 }()
 fmt.Println("1")
 sum := 0
 for i := 0; i < 666666666; i++ {
  sum += i
 }
 fmt.Printf("sum: %d\n", sum)
}

执行结果 _config.yml

原因 卧槽,说明main携程执行太快了。。。 因为就1个cpu啊,只能执行一个内核线程。 一个内核线程(GPM的M)某个时刻只能执行一个携程。 子携程还没来得及启动,main就完事退出了。

参考

2.Golang: 深入理解panic and recover

Go运行时的抢占式调度

本文的写作目的

首先,系统的理解Go调度相关的知识需要了解较多的底层知识,投入产出比很低,(明确自己的目的,知道不做什么很重要,人的精力真的是有限的,把每分每秒都压在刀刃上,才能更快达成目标不是么)

其次,对大部分软件而言,调度知识在工作中涉及很少,在软件设计,编码,测试阶段基本都不会涉及,只有性能优化的场景可能需要了解这块内容。

但是Go调度器的演变过程,是一个极好的性能优化的case,可以从中一瞥顶级程序员的改进和优化思路,令人上头。

我不希望在面试中背调度器八股文,而是希望能和读者或面试官探讨能从调度器的演变过程学到什么。

本文按照如下思路进行组织。

1.为什么Go需要一个调度系统,它需要做什么?

2.Go

Go语言调度器的实现机制是一个非常深入的话题,文末的附录3 推荐了几篇文章,特别值得探索学习。 我这里列举一下学习笔记。

Go语言调度器的领域分层图 1.什么情况下需要调度器 目前只有 Linux操作系统的进程调度,k8s,go runtime 2.

调度系统需求分析

我们在这一节中将从多个方面介绍调度系统设计时需要重点考虑的问题,其中包括调度系统的需求调研、调度原理以及架构设计。

  待分配的任务(Work) 可执行任务的资源(Resource)
操作系统的进程调度器(linux) Thread CPU 时间
容器编排系统(Kubernetes) Pod Node(CPU、GPU 和内存等)
go-runtime Goroutine 在 CPU 上运行的线程

协作式调度和抢占式调度

// 附录5

go为什么需要自己实现调度?

协作式调度依靠被调度方主动弃权;抢占式调度则依靠调度器强制将被调度方被动中断。

// 附录5 这两个概念其实描述了调度的两种截然不同的策略,这两种决策模式,在调度理论中其实已经研究得很透彻了。

func main() {
	//runtime.GOMAXPROCS(1)
	var ops uint64 = 0
	for i := 0; i < 50; i++ {
		go func() {
			for {
				atomic.AddUint64(&ops, 1)
				//fmt.Print()
			}
		}()
	}
	time.Sleep(time.Second)
	opsFinal := atomic.LoadUint64(&ops)
	fmt.Println("ops:", opsFinal)
}

参考

1.为何这段go程序不能让出控制权?

1.1为何这段go程序不能让出控制权? - 月明星稀的回答 - 知乎

2.Go: Goroutine and Preemption

3.关于 Go1.14,你一定想知道的性能提升与新特性-1.2 goroutine 支持异步抢占

4.Golang 汇编入门知识总结-NOSPLIT-TODO

5.6.8 协作与抢占

mysql面试题:orderby是怎么实现的 && MySQL默认是主键排序吗?

_config.yml

背景

做业务需求时,发现有主键排序的需求,但是排序可能导致sql扫描行数极端情况下暴增,可能导致性能问题。 但是线上版本没有加显示排序,新需求需要加上主键升序排序,所以产生了一下疑问:

sql:
select * from xxx where yyy limit 1;
sql: 极端情况下这个sql可能扫描几万行数然后需要选择id最小的一个。不知道新sql是否默认按照id升序查找第一个结果的。
select * from xxx where yyy order by id limit 1;

疑问

mysql是否存在默认按照主键升序排序的情况

结论

参考

1.深究mysql默认排序,order by的顺序 2.MySQL默认排序真的是按主键来排序的吗

不同隔离级别下的乐观锁有区别吗?

主要内容 1.不同隔离级别下的乐观锁有区别吗?(可重复读和读已提交) 2.当前读和锁等待的必然联系 3.当前读有哪些? 4.当前读,总是读取已经提交完成的最新版本。

当前读和锁等待的必然联系

因为当前读必须要读最新版本,如果此时该行数据上有事物尚未提交,则当前事务必须等待锁,这样才能保证读取到最新的记录.

一个无法更新的现象

Q: 事务隔离级别是可重复读。现在,我要把所有“字段 c 和 id 值相等的行”的 c 值清零,但是却发现了一个“诡异”的、改不掉的情况

Q:可重复读 下 乐观锁 还会有效吗? // 工作中遇到疑问

我看leader把select和udpate放一个事务里,这个乐观锁有点尴尬。

如果是可重复读的隔离级别,那么对当前事务来说,只有version = version的情况啊,这个是横成立的。 即使其他事务对version进行了修改,这个事务也是看不到的吧?

A: 发现可重复读的情况下, 乐观锁也是没问题。因为update是当前读。乐观锁在可重复读下的使用

Q: 乐观锁在可重复读下的使用的过程?

// 附录1

以下是技术群讨论后,2021-05-26更新

update包含了当前读

_config.yml

群讨论

_config.yml

以下是技2021-08-11更新

业务中使用的乐观锁 TODO

Q:什么业务要加锁,加乐观锁 or 悲观锁

Q:乐观锁怎么加

Q:悲观锁怎么加

乐观锁在可重复读的隔离级别下会有问题

乐观锁在可重复读的隔离级别下,每次重试都是快照读,永远不会更新成功,很多请求一直重试,则会导致事务锁等待超时。

update操作是当前,会加行锁,导致修改同一行的数据锁等待,多等几次就超时了。

// 附录4

解决方案: 乐观锁需要配合读已提交的隔离级别才可以,这样是当前读,可以在最新版本的基础上进行更新,才会成功。

1.使用版本号实现乐观锁的不足 TODO

// 附录3

秒杀等高并发场景下,每个版本只能成功1个,也就是每次只能有1个人抢购成功。 比如有库存100个,每人买1个,那么单个用户最多需要重试99次才能买到,但是正常是用户只需要抢购1次,如果有库存就能成功。 而不是有库存但是版本号不对则不能成功.

TODO Q:什么场景是必须在正确的版本号基础上修改才可以.

Q:什么场景不依赖版本号,只依赖某些限制条件(如:库存),则可以修改.

2.使用条件限制实现乐观锁

比如,库存模型和份额+-

参考资料

1.08 | 事务到底是隔离的还是不隔离的? 2.本机资料【169期】面试官:同学,分析一下MySQL_InnoDB的加锁过程吧-技术圈.pdf 3.使用mysql乐观锁解决并发问题 4.乐观锁加重试,并发更新数据库一条记录导致:Lock wait timeout exceeded

mysql排序时用到联合索引的后缀导致慢查询

_config.yml

SELECT ap.id, ap.product_id, ap.sku_id, ap.spu_id, ap.activity_id
	, ap.discount
FROM xxx ap
	JOIN yyy a ON a.id = ap.activity_id
WHERE a.activity_type = 'vip_discount'
	AND (ap.product_id IN (4099429, 4258467, 4531197, 4667974, 4677304, 4856925, 5140893, 6570089)
		OR ap.spu_id IN (4099429, 4258467, 4531197, 4667974, 4677304, 4856925, 5140893, 6570089))
	AND 1621664616 >= a.begin_time
	AND 1621664616 < a.end_time
ORDER BY ap.activity_id DESC // 修改为ORDER BY a.id DESC,速度即可变快

xxx的索引为

Create Table: CREATE TABLE `xxx` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自动编号',
  `activity_id` int(10) unsigned NOT NULL,
  `product_id` int(10) unsigned NOT NULL,
  `order` int(10) unsigned NOT NULL COMMENT '在列表中的顺序',
PRIMARY KEY (`id`),
KEY `aid_pid` (`activity_id`, `product_id`),
KEY `product_id_activity_id` (`product_id`, `activity_id`),
KEY `idx_create_time` (`create_time`),
KEY `idx_updated_at` (`updated_at`),
KEY `idx_sku_id` (`sku_id`),
KEY `idx_spu_id` (`spu_id`),
KEY `idx_activityid_order` (`activity_id`, `order`)
);

go-micro复习(1)

https://talkgo.org/t/topic/80

Micro是面向开发人员的微服务框架,如果人们用了K8s,那应该就不要用micro。

es存储原理-内部技术分享

// TODO es term 和term index 选择term时 与hash表的不同. fst

分两部分

1 es 使用的搜索引擎库 ,lucene是什么?

2 es在lucene基础上,做了哪些事情?

Lucene 是什么?

倒排索引 inverted index,指的是一种索引方法, 建立的是分词(Term)和文档(Document)之间的映射关系

相关的名词 index(索引):名词指的 一堆有相似结构的 文档数据,与mysql里的database 类似 动词指的是, insert的同时创建一个索引

文档 & 属性: 一个document是一条实例数据,类似mysql里的 行; 属性(field)类似于 表里的字段

Term 词条:最小的存储 和 查询单元,英文里的一个单词,中文里分词后的一个词

Term Index:可以快速地定位到Term Dictionary 的某个 key,加速 以及 节省内存空间 数据结构是变种的trie树 // TODO ydd 这里好像是fst数据结构,具体还要再看看

Term dictionary词典:词条Term 的集合,类似与哈希表 单词到 倒排列表之间的关系;key 是词, value是 倒排表,

Post list 倒排表 :记录 term 到 document的之间的关系

posting 倒排索引项: 倒排表里的每一项数据,包括了以下信息

1 文档ID 2 词频,这个term在这篇 document中的 出现的次数 3 位置position, term在文档中的位置,用于位置相关的搜索,phrase query 4 偏移位置offset,记录term开始结束的位置,实现高亮显示

索引到词之间的关系:

索引(Index)→ 段(Segment)→ 文档(Document)→ 域(Field)→ 词(Term)

Lucene 一个 Index 会包含多个 Segment; 一个 Segment 又由多个文件共同组成,记录的一组关系 及 原始数据:

xx.tip:存储 Term Index xx.tim:存储 Term Dictionary xx.doc:存储 Postings 的 DocId 信息和 Term 的词频

行存储 xx.fnm:存储文档 Field 的元信息 xx.fdx:存储文档的索引,使用 SkipList 来实现 xx.fdt:存储具体的文档

列存储,排序,聚合的功能、等 xx.dvm:存储 DocValues 元信息 xx.dvd:存储具体 DocValues 数据

lucene创建索引流程 1 Tokenizer:分词阶段 2 预处理,大小写 3 索引阶段,此阶段会创建字典、创建倒排表

lucene 搜索流程 1 分词 2 预处理 3 搜索,term-> term index-> term dic -> post list-> posting ,获取相关文档 4 结果排序

ElasticSearch 封装了哪些东西?

基于Lucene的分布式搜索引擎。 分布式 + 搜索

集群(cluster), 一组协同工作的服务器集合,用来提供比单一服务更稳定、更高效、更具扩展性的服务平台;

在外界来看,集群是一个独立的服务实体(透明的)

node的概念 两种节点,master节点、data节点

conf/elasticsearch.yml: node.master: true/false node.data: true/false master节点:可参加选举,PacificA 算法 实现cap原理,明细:https://www.alibabacloud.com/blog/elasticsearch-distributed-consistency-principles-analysis-3—data_594360)

data节点:只用做数据存储 既不是master,也不是data:类似proxy-like作用的节点,聚合结果返回给请求

另,集群中的任意节点,都可以接受请求

Shard 分片 1 是一个最小的 Lucene 索引单元, 是 Lucene 的一个实例

2 es集群支持水平扩展能力的方案;分片被分配到集群内的各个节点里;

3 主分片(primary shard)、副本分片(replicas)

主分片:每个分片有 数据量的上限,分片的数目,直接决定着集群中能够保存的最大数据量,不可更改 副本分片:拷贝,主要是用来实现高可用、高并发的,例如:硬件故障时保护数据不丢失,提供读服务等。

比如3节点,1主2副本, 每个节点被分配到一个分片;(kafka里partition分区的副本 分配方式类似)

数据同样会被 balance ,每个分片数据量大致趋同

与lucene的关系 es的每个Index 有个多个shard,每个shard相当于一个 Lucene实例; 而每个Lucene有个多个 segment,每个 Segment 意味着一组映射关系:

es索引原理?路由

shard = hash(routing) % number_of_primary_shards(分片数量) Routing是一个可变值,默认是文档的 _id,

意味着 1 一旦分片数量确定,不能变更,否则数据会错乱;只能重建,也就是,不是一致性哈希 2 拿到_id,每个节点都知道集群中任意一个文档所处的位置

写操作 1 根据公式计算,数据在哪个分片 2 先在主分片上面完成之后,将数据复制到其他节点的分片副本; 3 等其他所有节点返回后,返回写的结果

index、create、update 都是写操作

1 只有等到所有 副本返回(不一定写成功),才真正返回,也就是同步的方式,延迟取决于最慢的那个副本 2只要返回了,在主分片 还是 副本分片 ,都是可用的,

那么问题来了,会不会读到脏数据?比如3节点,主分片与其中1个副本成功,其中一个副本失败 当写失败时,主分片告知 master,master把这个有脏数据的副本(节点)摘除,也就是这个副本(节点)不能再读了

可能读到,但时间很短,或者说不是强一致性的

读,取文档 1 根据公式计算,数据在哪个分片 2 负载均衡获取一个副本(也可以是主分片),将请求转给这个副本 3 获取数据,返回, 也就是执行lucence相关操作

更新文档 1 根据公式计算,数据在哪个分片 2 请求转到主分片 3 修改 _source 字段中的 JSON ,并且尝试重新索引主分片的文档。 如果文档已经被另一个进程修改,它会重试步骤 3,超过 retry_on_conflict 次后放弃 4 如果节点 3 成功地更新文档,它将新版本的文档并行转发到其他节点上的副本分片,重新建立索引。 一旦所有副本分片都返回成功,节点 3 向协调节点也返回成功,协调节点向客户端返回成功

意味着update可能失败;

批量操作

mget操作、bulk操作,类似,区别并行操作,快一些

ElasticSearch 内部规则

1 协同多个shard(lucene)工作,而lucene本身高性能的全文检索工具包,倒排索引 2 倒排索引被写入磁盘后是不可改变的,如果你需要让一个新的文档可被搜索,需要重建整个索引。 更倾向于读,对变更数据的效率进行了妥协。

不变性意味着哪些好处, a 不用锁,b 缓存更容易维护,并且提高命中率,等等

3 避免单点故障,避免丢数据,通过translog 实现

动态更新索引

目的:为了保持不变性的前提下实现倒排索引的更新; 方式:通过追加新的补充索引来反映最近的修改,而不是直接重写整个倒排索引

注意:每一个倒排索引都会被轮流查询到,从最早的开始,查询完后再对结果进行合并 从索引中删除了大量文档,但这些文档只是做了删除标记,物理上并没有被删除

1 新的文档会被收集到内存索引缓存(In-memory buffer)中, 不可搜索

2 内存索引缓存生成一个新的 segment,先刷到文件系统缓存中; Lucene 这个时候是可以检索这个新的 segment 的; 刷新的过程就是refresh;

3 这个时候丢数据咋整?也就是到以上步骤时还没有落盘, ES写到内存索引缓存的同时 记录translog(linux 里write文件操作,延迟写); refresh发生,translog保持原样; refresh 完成后, 内存索引缓存被清空,但是事务日志不会;

4 translog 是什么?类似于mysql里的write-ahead-log

fsync才是真正的I/O操作,也就是落盘 es默认每隔一定时间(5秒钟)会把translog刷新(fsync)到磁盘中, 意味着这时候宕机了,会丢失这部分数据;

恢复方式:从当前的 commit point (包含了此前生成的translog) 生成 lucene实例

commit point 是啥? lucene,新增了commit point的概念,相当于一个标记(类似与版本) ,列出了所有已知的”段”的文件;

5 translog 什么时候主动flush?

两种方式,每隔一段时间 或 translog文件大小达到一定大小 比如,默认参数 30 分钟一次 flush,或者 translog 文件大小超过 500M 的时候

a 内存索引缓存被清空(refresh操作) b 表示日志全部落到磁盘fsync,创建一个新的translog,旧的删掉 c 生成一个新的commit point

段合并 每次refresh都产生一个新段(segment),频繁的refresh会导致段数量的暴增。 段数量过多,会消耗大量资源 a 消耗文件句柄 b 内存 c CPU时间 影响查询速度

所以Lucene干了什么事呢?通过 段的合并,来解决这个问题。

a 利用小的段合并到大的段,然后继续合并大的段,合并过程中会把已删除的文档从文件系统中清除, b 这个过程是自动运行的,开发人员无感知。 c 这个阶段如果有索引刷新操作,会创建新的段并将段打开,在合并过程中不会中断索引。 d 段合并结束后,删除旧的段

什么时候促发合并? 段的合并,这个动作本身,会消耗掉大量系统资源CPU,尤其是磁盘I/O 会有相应的策略,衡量资源的投入产出(段的大小、可回收的文档数量越多),执行段的合并

总结

TODO

参考

https://xie.infoq.cn/article/55095e9626718380c4072f5fb https://developer.aliyun.com/article/775303 https://www.infoq.cn/article/uotliglvj6tcue2vxc5x https://www.infoq.cn/article/DMw6z0XcCSgCWS6TyTF1 https://developer.aliyun.com/article/108070 https://www.elastic.co/guide/cn/elasticsearch/guide/current/inside-a-shard.html https://kkewwei.github.io/elasticsearch_learning/2019/10/17/ES%E6%AE%B5%E5%90%88%E5%B9%B6%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/

golang浮点数判断相等和大小比较的坑

1.golang比较浮点数是否相等

const Accuracy = 0.000001

// MIN 为用户自定义的比较精度
func IsFloat64Equal(a, b float64) bool {
	return math.Abs(a-b) < Accuracy
}

func IsFloat64Greater(a, b float64) bool {
	return math.Max(a, b) == a && math.Abs(a-b) > Accuracy
}

func IsFloat64Smaller(a, b float64) bool {
	return math.Max(a, b) == b && math.Abs(a-b) > Accuracy
}

func IsFloat64GreaterOrEqual(a, b float64) bool {
	return math.Max(a, b) == a || math.Abs(a-b) < Accuracy
}

func IsFloat64SmallerOrEqual(a, b float64) bool {
	return math.Max(a, b) == b || math.Abs(a-b) < Accuracy
}

Go踩坑-影子变量(Shadowed variables)

https://www.cnblogs.com/yudidi/p/14258565.html

影子变量(Shadowed variables)

Trie树的两种实现(图解)

My helpful screenshot

My helpful screenshot22

The easiest way to make your first post is to edit this one. Go into /_posts/ and update the Hello World markdown file. For more instructions head over to the Jekyll Now repository on GitHub.

量化交易-数据分析-pandas-均线斜率分析

均线策略

金叉,死叉

My helpful screenshot

My helpful screenshot22

The easiest way to make your first post is to edit this one. Go into /_posts/ and update the Hello World markdown file. For more instructions head over to the Jekyll Now repository on GitHub.

  • 参考

1.实战AI量化交易(3)-均线策略

Github+Jekyll+gitment 搭建个人博客(带评论系统)

感恩Github,这套系统不需要服务器和域名

2021-5-10 首次搭建,没有评论系统

按照附录1和2的博客做,因为这两个博客已经写得很完善了,我就不在赘述了。

遇到的问题

Q:配置图片

1.相对路径访问仓库的图片

![_config.yml](/images/jekyll-logo2.png)

效果: _config.yml

2.绝对路径访问外站图片

<iframe id="embed_dom" name="embed_dom" frameborder="0" style="display:block;width:1000px; height:600px;" src="https://www.processon.com/embed/606685cbe0b34d28298f2b03"></iframe>

2021-06-06 加入评论系统

具体步骤完全参考附录3,直接生效.

两点注意:

1.需要新注册一个github小号,在小号里注册oauth application,使用小号app的client secret.

2.生成github oauth的”Authorization callback URL”,填自己博客路径即可,比如我填写的https://yudidi.github.io/

_config.yml

第一个成功的评论: https://yudidi.github.io/pprof-heap-alloc/

2021-08-24 增加导航和分类,但是没成功,跳过

// 附录5,6

2021-08-28 通过全站COPY,照搬某站点的文章组织方式:导航和分类。同时用gitalk替换gitment评论系统

附录

1.按照这个博客说的做

2.进一步使用看官方文档

3.在Jekyll博客添加评论系统:gitment篇

4.本篇博客的源文件在这里,遇到问题可以参考我的源文件和历史提交记录,都是透明开源的

5.手把手教你使用jekyll正确设置文章分类导航栏

6.jekyll如何写一个导航栏? 搭建导航遇到一些问题,需要更多的理解Jekyll

7.搭建一个免费的,无限流量的Blog—-github Pages和Jekyll入门

祭出大绝招.搞了半天没搞出来,切换思路. 参考附录7的末尾,提到了 至此,最简单的Blog就算搭建完成了。进一步的完善,请参考Jekyll创始人的示例库,以及其他用Jekyll搭建的blog 所以我准备,参考一个有导航和评论的现成博客,源码全部复用即可.