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
GORM 提供了 SavePoint
、RollbackTo
来提供保存点以及回滚至保存点,例如:
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()
回滚事务
使用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 钩子
- 完成最终的数据处理
- **返回结果**
- 检查错误状态
- 返回查询结果给应用层
整个过程是一个链式调用的流程,通过回调系统实现了高度的可扩展性,允许在查询的不同阶段插入自定义逻辑。每个阶段都有明确的职责划分,共同协作完成数据库查询操作。
gorm进行query的时序图 基本能够很好的辅助一个初级研发看懂gorm v2的核心执行逻辑,主要是回调注册和回调执行的部分。
1.注册并登录成功后,只能试用min(2周,150次询问),达到限制后20刀每月
或许程序员和产品经理可以合并为1个岗位了。。。
类似Dexx的交易工具需要抓取和存储dex的大量交易对产生的大量交易数据,交易对数量可达数百万,区块中交易笔数每日可达百万级。 如何保证每个交易对的数据存储和查询效率,尤其是高频交易对(如ETH/USDT等)成为了亟待解决的问题。
用户侧主要有两种查询:
目前1年的交易数量约10亿数据,使用如下的分表方案
现有的分表策略中,交易对的交易记录是通过对交易对地址(PoolAddress
)进行哈希计算,然后将哈希值对256取余(PoolAddress % 256
),来决定该交易对的数据存储在trade_0
到trade_255
之间的256个分表中的一个。
分表路由规则:(PoolAddress(交易对地址字符串) % 256
),计算结果决定该交易对的数据存储在trade_0
到trade_255
的哪一个分表。
当前查询语句
where 交易对=?
where 交易对=? And 钱包地址=?
尽管分表方案能将大部分交易对的交易记录均匀分布到256个分表中,但热点交易对(如ETH/USDT)的查询频率和交易量可能会大幅度高于其他交易对。这样的数据分布依然可能造成一些分表(例如trade_0
)的负载过重,导致查询延迟增加,系统性能下降。
trade_100
)几乎没有负载,导致计算和存储资源的浪费。闲的闲死。。。为了优化查询性能和数据分布,提出手动调整分表路由的方案,针对热门交易对进行精确的手动调整,而无需进行二次分表。
针对热门交易对(例如ETH/USDT),我们可以选择通过手动调整路由来优化分表分布。即将这些热门交易对的交易记录迁移到新的分表中,从而减轻原有分表(如trade_0
)的查询压力。具体过程如下:
识别热点交易对,规划新的分表-> 入库双写+数据迁移 -> 查旧表改为查新表
为确保迁移过程中的数据一致性和查询不中断,我们采用双写策略来实现平滑迁移。迁移的具体步骤如下:
启用双写:修改爬虫入库逻辑,确保所有新的交易记录同时写入旧的分表(例如trade_0
)和新的分表(例如trade_256
)。这样可以确保新交易记录在新分表不丢失。
迁移历史数据:将热点交易对的历史交易记录从原有分表(如trade_0
)迁移到新的分表(如trade_256
)。编写脚本,分批将历史数据迁移到新分表。
确认数据一致性:验证新旧分表中的数据一致,避免数据丢失或重复。
切换查询路由:历史数据迁移完成后,将查询路由切换到新的分表。此时,所有关于ETH/USDT的查询将直接访问trade_256
,而不再访问原分表。
停止双写:在确认迁移完成并且新分表已经承载查询流量后,停止双写模式。此时,所有数据都只写入新的分表。
清理旧数据:一旦确认新的分表完全接管了查询请求,可以将旧分表中的相关数据删除,释放存储资源。
通过手动调整路由分表和数据迁移,可以有效解决由于热点交易对造成的数据倾斜问题。 相比于复杂的二次分表方案,这种方法在操作上更为简单和灵活,并能高效应对去中心化交易所中高频查询的挑战。
slot0返回的
对应的池子 https://app.uniswap.org/positions/v3/ethereum_sepolia/30563
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.
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)
SetConnMaxIdleTime(30 * time.Minute):连接在空闲状态下最长保持时间为 30 分钟。 SetMaxIdleConns(10):每个进程最大空闲连接数为 10。 这2个的关系,如果10个空闲连接,30分钟没有使用,是不是会销毁,然后重建10个新的空闲连接呢?
这两个设置项 SetConnMaxIdleTime
和 SetMaxIdleConns
共同作用于数据库连接池的管理,但它们的作用是不同的,需要明确理解它们的关系和相互影响。
SetMaxIdleConns
这个配置项指定了连接池中可以保持的最大空闲连接数。即使在没有任何查询请求的情况下,连接池也会保持最多 maxIdleConns
个空闲连接,以便在有新的查询请求时能够快速响应而不需要重新建立连接。
SetConnMaxIdleTime
这个配置项指定了连接在连接池中处于空闲状态的最长时间。如果一个连接空闲超过 maxIdleTime
,那么这个连接将被关闭和移除。
空闲连接的数量管理:SetMaxIdleConns
控制的是连接池中允许存在的最大空闲连接数量。例如,如果你设置了 SetMaxIdleConns(10)
,那么即使在没有任何数据库操作的情况下,连接池也会保持最多 10 个空闲连接。
空闲连接的时间管理:SetConnMaxIdleTime
控制的是每个连接在空闲状态下的最大保持时间。例如,如果你设置了 SetConnMaxIdleTime(30 * time.Minute)
,那么每个空闲连接如果持续 30 分钟没有被使用,就会被关闭。
假设你设置了以下内容:
mysqlDB.SetMaxIdleConns(10)
mysqlDB.SetConnMaxIdleTime(30 * time.Minute)
在实际应用中,这两个参数的配置需要根据你的具体负载和数据库性能来调整。
maxIdleConns
以提高响应速度,同时设置一个合理的 maxIdleTime
以防止过期连接占用资源。maxIdleConns
和 maxIdleTime
,以减少数据库连接的资源占用。通过合理配置 SetMaxIdleConns
和 SetConnMaxIdleTime
,你可以确保数据库连接池在性能和资源利用之间达到一个平衡。
在讨论空闲连接的行为之前,需要明确几点:
SetMaxIdleConns
):这是连接池中允许保持的最大空闲连接数。如果空闲连接多于这个数目,多余的连接将被关闭。SetConnMaxIdleTime
):这是一个空闲连接在没有被使用的情况下可以保留的最长时间。超过这个时间,连接将被关闭。当你刚启动你的应用程序时,并没有任何数据库查询请求进来,此时连接池中没有任何空闲连接。SetMaxIdleConns(10)
并不意味着在应用程序启动时就会立即创建 10 个空闲连接。
SetMaxIdleConns
。SetConnMaxIdleTime
,这些连接将被关闭。SetMaxIdleConns
个空闲连接,但不会主动创建空闲连接以达到这个数目。连接池中的空闲连接数是根据实际需求动态调整的。假设你设置了:
mysqlDB.SetMaxIdleConns(10)
mysqlDB.SetConnMaxIdleTime(30 * time.Minute)
SetMaxIdleConns
:连接池不会在没有任何查询请求的情况下主动创建空闲连接来达到 SetMaxIdleConns
的数量。SetConnMaxIdleTime
的控制。SetConnMaxIdleTime
的设置,自动关闭超过空闲时间的连接,以确保资源不被长期占用。这样配置的目的是在高效利用资源和确保查询响应速度之间找到一个平衡点。
在数据库连接池管理中,新的连接会在以下几种情况下被创建:
SetMaxOpenConns
设置),则会创建一个新的连接来处理这个请求。SetMaxOpenConns
。在 Go 语言中使用 database/sql
包时,标准库本身并不直接提供方法来实时监控当前连接池的状态(比如当前有多少个连接正在使用,有多少个是空闲的)。但是,有几个方法可以间接帮助你了解连接池的状态:
SetMaxOpenConns
:设置最大打开连接数。SetMaxIdleConns
:设置最大空闲连接数。SetConnMaxIdleTime
:设置连接的最大空闲时间。如果你使用的是 MySQL,可以执行以下 SQL 查询来查看当前的连接数:
SHOW STATUS LIKE 'Threads_connected';
这个查询会返回当前连接到 MySQL 服务器的线程数,这实际上代表了当前的连接数。
在使用MySQL和MongoDB时,虽然数据库引擎通常会自动选择最优的索引来执行查询,但有些情况下可能需要强制指定索引,以确保查询性能。这些场景包括但不限于以下几种:
FORCE INDEX
强制使用特定的索引。例如:
SELECT * FROM my_table FORCE INDEX (index_name) WHERE column = 'value';
覆盖索引 当一个索引包含所有需要查询的列时,可以通过指定索引来强制使用覆盖索引,这样可以避免访问表的数据行,提高查询速度。
hint
方法来强制指定一个索引。例如:
db.collection.find({field: value}).hint("index_name");
调试和性能测试
在调试和性能测试时,可以通过 hint
强制使用特定的索引,从而分析不同索引对查询性能的影响。
虽然数据库引擎通常能够自动选择最优索引,但在某些特定场景下,强制指定索引是必要的。无论是MySQL还是MongoDB,合理使用索引能显著提升查询性能,但也需要谨慎,避免对数据库性能产生负面影响。
用mv命令即可
# 备份+覆盖
cp cron cron.back && mv cron /data/bin/cron
是否开启保活。
空闲多久后开始保活探测
多久发送一个词保活探测。
最大探测多少次后可以判断连接空闲,可以关闭。
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包关闭连接。
A: 1.在访问数据时,数据会先加载到os缓存,然后再加载到shared_buffers,这个加载过程可能是一些查询,也可以使用pg_prewarm预热缓存。
A: PostgreSQL缓存读顺序share_buffers -> 操作系统缓存 -> 硬盘。
那么也可能是操作系统缓存不足,而不定是share_buffers。
通过文章中vmstat命令看到cache有260G,free值也很稳定,所以应该检查PostgreSQL的缓存。(老师执行vmstat是不是埋了个伏笔)。
我发现我本地磁盘使用了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
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 负载高场景测试
4.top命令输出解释以及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 完成就可能立即变为可运行状态,它们对系统负载的影响是有意义的。
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).
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.组合嵌套模版
TODO
// 附录2
// 附录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 程序的运行效率。
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部署。
// 附录4
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
1.对单个 file/socket 的并发操作个数超过了系统上限,这个报错是 fmt.Printf 函数引起
2.文件句柄数是有限的
3.内存是有限的
#
从平时的不到0.5ms变为3ms,扩大6倍
old分区应该是2g。
差不多30分钟-90分钟就会从900MB达到1.8G触发old gc
注意到,Old区的内存持续增长,30-90分钟内存就会将Old区填满,经过Old GC几乎全部可以回收掉一半。
每次在old区快用完时,gc频次监控都有个尖刺。
4.注意到同时也有full gc
1.es大量写入,refresh和fsync操作比较费资源,导致es负载增加
最近做搜索推荐,线上预测服务需要读取大量的特征,对内存缓存的需求比较,所以调研了一下go内存缓存相关库.
1.是否零gc, 无gc原理
2.expire原理
3.数据淘汰机制
4.优势,劣势
最近公司把服务部署都迁移到k8s上了,我对docker和k8s进行了一定的复习。
突然想到很久之前记录的关于 docker无法隔离io 的经历。 特此记录到博客中,以免未来忘了这事。
原来docker不能隔离io,是因为所有容器都共享了bufferdio的原因。
我理解是如果一个高io容器占据了全部的 Bufferdio,那么肯定会导致其他容器无法使用Bufferdio。
这个问题是我在成都的公司遇到的,就是我们的容器无法隔离io,只要有一台容器频繁的访问磁盘,就会导致其他容器变慢,现在看来确实是这样。
除了常见的 CPU、内存外,还有 IO 的限制
、ulimit 限制、PID 限制等等。所以我们扩展了 K8s 来完成这些工作。
对 core dump 的流程进行了修改,让 dump 文件写到容器自身的文件系统中,并且使用容器自己的 Cgroup IO 吞吐限制。
一般业务开发会经常有对某个进行crud的诉求,这个其实是可以做配置来提供增删改查的。
我之前公司的后台管理平台是可以做到UI配置化来支持对某个表的crud的。
golang这边找到一个开源的代码,支持配置文件:takoyaki
docker logs 命令会吧容器内的stdout、stderr 标准输出和标准错误都显示出来。具体原理见下文
# 查看qp容器的最新日志
docker logs --tail 10 qp -f
#
第39周 数据库监控与调优
本周我们来一起解决数据库监控与调优的问题,通过本周的学习,基本上可以搞定慢查询,索引等数据库重难点性能调优问题。
课程安排:
1、数据库监控 - Prometheus
2、慢查询日志
3、索引分析与优化
4、锁优化,Query Profiler
5、SQL优化工具
6、MySQL配置优化
7、拓展分享:如何在合适的场景用合适的数据库?
8、拓展分享:业务妥协会对系统调优造成怎样的影响?
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
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
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分钟
原因定位过程
最开始是以为有什么上线,导致数据增长比较多。所以问了下3个业务组最近有哪些上线。结果没有上上线。
因为我们线上是一主三从,然后一个同事发现, 只有主库报警,从库没有报警,主库磁盘占用 比 从库磁盘占用 多了60G。
然后阿里云提了工单,给我们看了mysql8数据库的数据库文件相关的信息
// 我做了一定处理
库 269G no
.库/表.ibd 14G
.库/表.ibd 13G
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表数据文件,覆盖掉原本改动过的数据。
// 附录7
没啥好办法,大数据量,回滚就是很耗时。
可以考虑分批提交。
// 参考附录3
1.1 回滚日志总不能一直保留吧,什么时候删除呢?
1.2 回滚段被清理,文件也不会变小? TODO 这是为啥
1.3 基于上面的说明,我们来讨论一下为什么建议你尽量不要使用长事务。 长事务意味着系统里面会存在很老的事务视图。由于这些事务随时可能访问数据库里面的任何数据,所以这个事务提交之前,数据库里面它可能用到的回滚记录都必须保留,这就会导致大量占用存储空间。
2.参考我之前博客
4.万答#18,MySQL8.0 如何快速回收膨胀的UNDO表空间
5.32 | 为什么还有kill不掉的语句? 所以,如果你发现一个线程处于 Killed 状态,你可以做的事情就是,通过影响系统环境,让这个 Killed 状态尽快结束。
在MySQL中,kill命令分为如下两种:
1、kill query + pid
2、kill connection + pid 其中connection可以省略
// 附录2
SELECT * FROM information_schema.INNODB_TRX\G
// 附录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后回滚整个过程解析
最近公司有战略合作,得到了某中型电商公司的技术支援.提供了部分搜索系统源码(java)。 其中设计图执行引擎相关的东西,所以我用go试一试图数据结构的实现。
1.Go Data Structures: Graph 2.Graph algorithms and data structures
最近在做搜索架构重构,在调研如何自己实现分词服务的方法。 需求是可以保持ik主要的分词逻辑,同时又能定制一些分词逻辑。
实现思路:
找ik源码改下(java语言),额外独立一个分词服务,在服务内部做代码调整,这样方便改动和部署上线。
附录1
// 附录1 总的来说
2.IK分词器优化
最近在做搜索推荐系统的架构重构,参考了转转公司的架构方案。 现阶段正在进行一些技术和存储的选型。
这篇文章记录缓存存储的选型。
Redis集群方案目前主流的有三种,分别是Twemproxy、Codis和Redis Cluster。
附录1讲述了高可用redis集群方案的演进过程
go-micro源码中和我们的线上代码大量用到了option模式。 发现一篇博客总结得全面,这里记录一下读后感。
最近团队项目增多,频繁遇到内存泄漏,携程泄漏等问题。我们对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
// 附录2 heap: 包含每个 goroutine 分配大小,分配堆栈等。每分配 runtime.MemProfileRate(默认为512K) 个字节进行一次数据采样。
附录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");
}
附录3,4
现在你就可以比较这两个时间点的堆的差异了: go tool pprof –base base.heap current.heap 操作和正常的go tool pprof操作一样, 比如使用top查看使用堆内存最多的几处地方的内存增删情况
但是, 分配堆内存比较多的地方并不一定产生内存泄漏,只能说明这个地方”曾经/正在”分配的堆内存比较大,或者分配的堆内存比较频繁俄安,这些分配的内存可能在之后就回收掉了。
像Java的一些profiler工具一样, pprof也可以比较两个时间点的分配的内存的差值,通过比较差值,就容易看到哪些地方产生的内存”残留”的比较多,没有被内存释放,极有可能是内存泄漏的点。
使用多年的go pprof检查内存泄漏的方法居然是错的—对比2个heap的对象变化
最近有个线上推荐服务,是读取1个1-2的模型文件,进行预测,然后提供对多路召回的商品进行实时排序。
性能现象:
1.qps<10,单个接口<100ms。
2.qps>10,单个接口马上400-500ms。
3.内存使用率50%, cpu使用率60-70%, 所以不是资源不足的问题。
我: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.调整模型数据结构。消除字符串拼接
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。// 也就是某个函数本身直接执行的耗时,不包括其子流程的耗时
内部项目(notify-srv-codeup)存在携程泄漏 4188ece1 jzw xxxx@qq.com on 2022/2/18 at 2:07 下午 从消息队列获取消息,每个消息产生一个携程,go监控的当前携程数量和消息发送数量相当。原因比较明细,但是该项目携程嵌套使用层次比较深。具体定位不到哪一行。
#
5.实战Go内存泄露
1.docker容器占用磁盘大,如何做数据迁移。TODO 应该是迁移容器层的东西?
2.OverlayFS 是怎么工作
测试环境用docker运行了很多容器。且几年前安装时默认使用的系统盘存储。 想迁移到数据盘中。
附录3,4 分别对应软链接和移动全部数据的方式
// 附录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
某个业务想增加一个监控指标。
就是每天不同渠道的老客唤起数量。 每日需要重置,然后更新,然后再累加。
而 Prometheus Metrics 有四种基本的 type:
Counter: 只增不减的单变量
Gauge:可增可减的单变量 // 看起来需要这个变量
Histogram:多桶统计的多变量
Summary:聚合统计的多变量
#
在业务场景中,无限制地允许命令使用可能带来诸多问题。一些Redis命令会直接清空大量甚至全部数据,例如flushall、flushdb等;keys、hgetall等命令的不当使用则会阻塞单线程的Redis服务,降低Redis服务的性能。
为保障业务稳定、高效率地运行,您可以根据实际情况禁用特定的命令,降低业务风险。
附录2,hscan
#
大家使用工具生成 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编译的服务读取这个缓存时是有问题的
我在公司负责营销服务,经常有营销规则的变化。
目前想要引入一个规则引擎来提高研发效率。
最近看同事写的linux-shell脚本,发现好多语法看不懂.这里补习一下.
shell函数调用
拼接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 {} \;
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
// 附录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 -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在匹配行之前显示文件名?
使用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的处理没有理解得很好。
今天数据团队: 大量抽取主库数据,导致线上部分数据库交互功能无法使用。
以下是同事写的事故报告和经验总结:
今天下午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线程卡住,主备延迟。
最近做商品全量静态化的线上测试.偶尔需要清空全量缓存。 因为之前有同事线上执行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-5.0_0.5.7
对于清空数据库命令flushall/flushdb,添加了async异步清理选项,使得redis在清空数据库时都是异步操作。
实现逻辑是为数据库新建一个新的空的字典,把原有旧的数据库字典给后台线程来逐一删除其中的数据,释放内存。
今天有同事整了个事故:
alter table修改枚举值,比如从a,b,c修改为 d,f,e,那么会删除掉旧的a,b,c的记录。
我看是同事写错了字段,把枚举字段A写成了另一个枚举字段B,然后B的枚举值,就覆盖到A字段了,相当字段A的枚举值被删除。
我一直以为alter table不会导致数据删除来着,同事的这个事故倒是弥补了我的盲区。
{
"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刷盘间隔。
公司内部分享了Kratos开源框架,其中有一些思想值得借鉴.
// 附录1
1.Redis 是基础组件,往往会在项目的很多地方被依赖, 那么如果哪天我们想整体修改 redis sdk 的甚至想把 redis 整体替换成 mysql 时,需要在每个被用到的地方都进行修改,耗时耗力还容易出错。
2.很难对 App 这个类写单元测试,因为我们需要创建一个真实的 redis.Client。
1.编码违背直觉? 2.可阅读性下降?
1.从 Kratos 设计看 Go 微服务工程实践-Go 的依赖注入框架有两类 2.Fx: Golang中的依赖注入(Dependency Injection)工具(翻译)
偶然看到一篇性能优化博客,里面记录了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 ‘END{print $2}’ yourfile 打印指定行的指定列(例如第4行第2列): awk ‘NR==4{print $2}’ yourfile
有时awk的print$是按照空格区分的,如果日志里有有个字段内容包含空格,则分割会错误。 有时发现可以从后往前分割就可以了。 // 附录6
// 附录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 -F ' ' '{print $2, $5}' | uniq -c
awk -F ',' '{print $1}' | uniq -c
# 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 -F ' ' '{print $2, $5}' | uniq -c
awk -F ',' '{print $1}' | uniq -c
* linux之awk命令获取最后一列
awk -F',' '{print $NF}'
* 统计耗时分布,需要先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
测试文本
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
# 排序并进行了出现次数统计
按照指定列排序
[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
$ cat file | awk '{a[$1]++}END{for (i in a){print i, a[i]}}'
awk '{sum += $1};END {print sum}'
$NF 可用来表示一个记录的最后一个栏位
NF表示当前行的字段数,$0表示当前行
求和题
文本文件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 正则截取字符串实例
我们内部的一个管理系统,业务代码量超过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/应用层/基础设施层不是领域模型关注的。上面的领域模型应用于分层架构中的领域层。
核心就是这么几句话了。
因为我在3年前待的公司,主要是做微服务的二次重构(前期重要抢占市场,技术架构做得不好,业务发展还可以,所以才需要二次重构)。 当时公司的微服务包括Go和Java两个系列,另外当时我也了解了DDD,当时并没有找到结合实践的实现方式。
// 附录2,3 发现一个很好的分享. DDD的实例
1.领域驱动例子 2.COLA 2.0整体架构 https://toutiao.io/posts/9b2wg1d/preview 2.5.DDD的模式与实践案例 3.COLA
电商商品流转过程中,商品状态很多,且有很多变化。 目前我们在商品状态时,会往MQ中投递一些消息。
因为topic是收费的,所以我们重要业务会有单独的topic。 其他业务都尽量使用1个topic,然后通过tag来区分消息,这样客户端在拉取的时候可以通过tag在MQ服务端就进行过滤。
现在的问题是: 其他业务使用1个topic,肯定是不合理的,tag会太多了。所以应该有多个topic。 那么自然就有如下问题: 哪些消息应该放到哪一个topic中。这个划分原则是什么?
目前的想法: 这个划分原则应该某种是业务领域。 划分业务领域,一是业务自身的边界,二是我们对业务的理解,三是按照领域驱动设计,其他的域划分。
不论是哪种方法,都需要我们对业务流程进行较为全面的梳理。
然后把业务流程划分为比较合适的子流程。 同时明确其中的领域事件。
整个过程不是一触而就的,需要在一开始设计时,就意识到,划分结果是不断变化和演进的。
以下图电商交易场景为例,从客户下单到收到商品这一过程会生产一系列消息,以以下消息为例: 订单消息 支付消息 物流消息 这些消息会发送到Trade_TopicTopic中,被各个不同的系统所订阅,以以下系统为例:
商品状态变化: 上架,下架,编辑中。
1。一个必要的准备工作,应该是领域事件,更具体的,就是消息需要定下来。 并且这些消息要能推动整个流程的完成。// 场景测试。 2。评价准备工作的标准: 核心的场景流程定下来。 不管最终设计的结果,都需要用这些场景过一遍。
最近在做重构类型的工作,更多的偏向业务架构类型的工作内容。 据我总结,我们需要理解业务,划分有哪些模块,明确这些模块间的关系。才能具备设计一个的合理的架构设计的基础。
如何做到这些,需要一定的方法论进行支撑。
这篇博客列举和总结一些我觉得有用的方法论。
// 附录1 我建议,设计架构、考察模块之间关系时,不要用“耦合”、“乱”这些无法度量的词语, 而应该改用以下三个可以度量的指标:依赖、正交性、紧凑性。
// 附录2 代码耦合的本质是一方对另一方的假设。两方之间的假设越多,两方的耦合度就越高。当然现实中,往往会遇到多方耦合。
本文要感谢我职级评定过程中的一位评委,他建议把之前所做的各种性能优化的案例和方案加以提炼、总结,以文档的形式沉淀下来,并在内部进行分享。力求达到如下效果:
1. 形成可实践、可借鉴、可参考的各种性能优化的方案以及选型考虑点,同时配合具体的真实案例,其他人遇到相似问题时,不用从零开始。
2. 有助于开阔视野,除了性能优化之外,也能提供通用的常见思路以及方案选型的考虑点,帮助大家培养在方案选型时的意识、思维以及做各种权衡的能力。
文章在内部分享后,引起强烈分享,得到了不少同事和朋友的认可和好评,觉得对日常的工作有很好的指导作用。考虑到这些经验可能对业界同行也有帮助,所以在美团点评技术团队博客公开。
目前接手过的业务, 优惠券和广告,都存在单表大表的的情况。
优惠券: 单表5亿+,大几十G。查询操作较多: 发券,领券 都需要进行查询和写入操作。
广告: 单表 5kw+,40G+。 CRUD qps > 1000。
最近在做电商商品列表页和详情页的价格缓存. 在设计缓存数据结构时,用到了hash结构。
这里记录一下redis hash的主要的使用场景.
具体参考附录1,非常详细的描述了redis的hash解决的问题,以及其底层的存储结构。
背景: 存放用户搜索的主词和后继词.
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
技术群遇到一个面试题分享:
Q: 为什么优先从p的freeg中获取,这样做有什么优点?
他描述的问题应该是为什么优先从本地队列拿,而不是steal或者全局。
当一个g执行完成之后,并不是直接收回g的内存空间,而是把g加入到p所对应的freeg列表当中,这就是空闲的g。
操作全局队列要加锁。如果是为啥要从本地队列里面拿,好像是为了避免多个p竞争锁?
我在线上使用这个库的代码,在看该库的实现的时候,发现了不常见函数的使用场景, 提了一个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.sum机制。//附录2
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左右,所以负载较高
升级前情况:
从kibana可以看到es节点的监控,升级后监控(cpu使用率和负载)如图:
es机器配置升级后搜索延迟为10-15ms,升级前是30-40ms(升级的忘记截图了)
es机器配置升级前后-节点负载对比
es机器配置升级前后-cpu使用率对比
内存和磁盘都够用
2022年10月,升级cpu,4核8g三节点 -> 8核8g三节点
阅读同事代码时,发现了这个库. 预计最近一个项目能用到. 记录一下.
// 附录1 很多时候,解析来自多种源头的数据流时,我们一般事先并不知道他们对应的具体类型。只有读取到一些字段之后才能做出判断。
实际上,Google Protobuf 通常也使用这种方式。 在协议中添加消息 ID 或全限定消息名。接收方收到数据后,先读取协议 ID 或全限定消息名。 然后调用 Protobuf 的解码方法将其解码为对应的Message结构。
从这个角度来看,mapstructure也可以用于网络消息解码
,如果你不考虑性能的话😄。
// 附录2
// 附录3 消费者原理分析-RocketMQ知识体系(四) 前文了解了 RocketMQ消息存储的相关原理,本文将讲讲消息消费的过程及相关概念。
线上报错
更新商品时出现 invalid connection 的错误.
查看阿里云慢sql日志,发现都是COMMIT语句,看不到sql,如图:
查看自动提交配置
DB默认是自动提交的,不存在”COMMIT”; 既然是commit,肯定是会话被关闭了自动提交;
但是为什么没有记录对应的语句,而只记录了COMMIT;呢 ?
1.IO不足导致COMMIT出现在SLOW LOG中. 2.业务启用了大事务, 例如BEGIN开启事务后,里面执行了大量的INSERT语句,然后再做COMMIT提交;
什么时候用RocketMQ队列,什么时候用redis队列
或者说持久化,不丢消息,何时仍MQ,何时仍redis(list,hash)
最近在做一个较大的性能优化,就是把商品的价格从实时计算改为离线计算。 这需要捕捉到所有影响商品价格的因子,这些因子主要是mysql的表. 所以考虑使用我们现有的canal来捕捉binlog变化,进而刷新价格.
但是商品数量是比较大,大几十万,不知道canal投递MQ的性能和消费的性能如何,所以需要测试。
RocketMQ有pull,puss两种消费模式。
pull每次能拉16
push则不受限制
附录1,2,3 // 附录4
2.整个push模式的处理过程,其中几个关键点: // 附录2 通过pull模式实现 主动发起消息的接取,RebalanceImpl类的子类RebalancePushImpl是拉取消息的入口 在rebalance后会触发消息的拉取 拉取成功后,调用listener的consumeMessage方法
# 1.消息队列推拉push/pull模式优缺点及使用改进的长轮询模式的意义 2.RocketMQ的push模式机制 3.MQ中Pull和Push的两种消费方式 4.首页>消息队列RocketMQ版>最佳实践>消息堆积和延迟问题-产品详情-消息堆积和延迟问题
负责的广告下发服务,单机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个步骤执行
current read
return && lock
delete row
success
在广告被刷量的情况下,单个设备did=x可能会在1秒内,请求1000次.
极端情况,可能1000个携程都执行到了step1,但是只有1个携程(1)成功,其他携程会因为行锁阻塞在step2上。
待携程(1)执行完step2,其他携程依次执行step2(删除不报错,但是RowsAffected=0),step3,step4
所以虽然执行了1000次删除语句,但是只删除了1条记录,但是我们最多可能写了999条记录。
// 附录2
# 1.16. 完善网络指标监控 + 7. 服务端需要限制最大连接数 2.golang官方实现如何对httpserver做频率限制(最大连接数限制)
# 普通索引 & 唯一索引 区别:
select 忽略不计 主要体现在update,普通索引不需要判断唯一性约束 change buffer,准确的说不在buffer中,有实质的差异,普通索引具备加速作用,在写多读少的场景
公司发展还可以,业务和系统规模有了较大增长,技术团队得到扩招。
某些业务也需要完善的分布式事务方案来支持,不能只靠定时任务扫表这种替代方案来搞。
这也标志着公司的微服务化改造开启了从60分到90分的新进步。
重构时,从旧服务对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”
最近将广告下发从php迁移到了go,以微博为例,请求qps最大可能能超过1k,如果算上其他的几个渠道,qps峰值可以超过3k.
每个请求基本涉及1次数据库插入,3k/s的速度写入我们线上的polardb,基本就把机器挂了。
针对这个问题,虽然已经根据业务特点,采用了异步写入+批量写入的优化方式,把对数据库的写入操作qps减少了N倍(N=每批量的写入数量)。
当时出方案时,考虑了异步写入的弊端,就是异步写入总是有可能存在不一致的(回复写入成功,但是程序崩溃了)。 就是说如果业务不允许异步写入,那要怎么办呢,如何可以保证100%的一致性呢。
今天我在看一篇博客时(附录2),看到了nosql相关的场景描述,发现这也是一个方案(我这里特指mongodb). 在我遇到的场景中:
nosql的优势:写入数据很快(待确认),存储的数据库很大()
nosql的劣势:查询语法学习成本,公司未使用mongodb需要新购买和维护
我负责的广告下发业务. 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.
consul是之前同事部署的,我不熟悉,最近遇到相关问题,发现还是需要学习一下子.
consul在微服务中的作用
consul架构 和 部署结构
go-micro涉及的consul相关的代码
go-micro的服务注册和发现组件
// 附录2
consul agent 分为 server 和 client, 一个节点2种角色.
我们分析一下,为什么第5步和第6步需要加-rpc-addr 选项,而第4步不需要加任何选项呢? 原因是-client 指定了客户端接口的绑定地址,包括:HTTP、DNS、RPC,而consul join 、consul members 都是通过RPC与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
公司内网的部分资料
在实际生产实践中,日志相关的问题出现过很多次,这里把问题和方案做一次梳理
Q: 哪些东西应该记录日志.
A: 以可以还原业务流程,便于分析定位线上问题,为准.
监控性质和非业务日志应该尽可能少
A: 项目初期上线时,未知bug可能较多,日志尽可能的丰富一些. 项目稳定后可以逐渐减少日志.
Q: 日志内容很多怎么办
A: 建立软连接把日志文件链接到大容量的磁盘. // 存储相对便宜,对于大部分项目,这个成本任何规模的公司都可以接受。
Q: 对于并发量大的项目的日志,日志不是顺序打印的,难以跟踪单个请求的完整业务日志.
A: 需要给每个请求增加唯一id,在日志中打印出来,用来串联一个请求的完整业务日志.
Q: 调试性质的日志,如何处理
A: 算法调试,
1.技术群有新人提问: 有大佬能用简单易懂的方式向面试官解释倒排索引吗?
2.有个新同事新接触es,问我倒排索引怎么理解.
3.之前有同事面试,也被问过该问题, 所以写篇博客记录一下.
一本字典,我给你一个字,你需要告诉我这个字在字典的哪一页。 我理解这就是正排索引。
一本字典,我给你一个词,你需要告诉我这个词在字典的哪些页出现了。 你需要设计一个怎么样的数据结构来处理这个问题。 这就是倒排索引。
1.[Go] 基础系列一: for-select中的break、continue和return 2.漫画趣解:透析Elasticsearch原理
主要有2个场景会用到wildcard,但是处于性能考虑,都没有采用wildcard的方案
用户搜索的场景: 因为词典导致分词结果不一致,此时可以用wildcard进行类似mysq like的方式来召回。
公司内部运营选品的场景: 希望输入n个词语,然后召回商品名字包含这些词的哪些商品,这些词之间是或的关系。 比如: 输入 a|b|c 三个词, 希望召回商品标题里面包含a或b或c的那些商品.
这个走分词搜索的逻辑,仍然会有词典导致的分词问题,所以考虑wildcard。 不过因为性能的顾虑,最终放弃了该方案,最终方案,是拆分为n次搜索,每次一个词语召回商品,然后合并商品,
#
我负责迁移的广告下发服务,提供广告下发的监测链接接口. 流量波动特别,qps多时可以上千。旧服务是每次接口调用都写1次数据库,导致出现了线上事故,所以我这边进行优化。
优化思路:
将下发消息进行聚合后,批量写入数据库,这样可以极大的减少数据库操作.
f := float64(2.9) + float64(1.11) - float64(4.01) fmt.Println(f)
线上结果是float64(-4.440892098500626e-16),本地是0
和编译器有关系
同样的操作 你在mac下面和linux下面可能数据不一样
#
TODO
可能的问题
需要停止服务
能否用脚本一键操作?
# 停止服务
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
取决于目标目录是否存在。存在则移动,不存在则无法移动就是改名 附录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 命令
今天线上除了事故导致APP挂掉. 原因是阿里云数据库被大量查询访问导致超载,响应慢。
我们的调用链路是:
1.APP -> 2.php服务端 -> 3.Go基础服务端 -> 4.阿里云数据库
关于超时,可以把开发者分为两类:一类是了解超时多么难以捉摸的人,另一类是正在感受超时如何难以捉摸的人。
最近有个新服务需要部署,不属于之前的微服务体系,是离职的算法同事的项目,侧重数据离线计算的一个项目.
部署需要用到nginx做轮询转发,所以学习nginx。工作四年才在线上用到nginx,所以现在才专门学一下。。。
# 安装 Nginx 时可以选择配置文件的路径,由于 Nginx 程序后续可能会升级版本,但配置基本上就是一份,那么推荐使用配置和程序分离的方式,遵循:
配置文件独立管理, 不存放在nginx程序目录内 每个站点独立一个配置文件 每个站点独立的日志文件 提取公用的配置文件
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
推荐算法的同事交接了部分算法的项目给我。
该部分业务的流程类似如下逻辑:
本来的实现是:
发现可以采用这个模式进行处理,提升性能。
// 附录2 FAN-OUT模式:多个goroutine从同一个通道读取数据,直到该通道关闭。OUT是一种张开的模式,所以又被称为扇出,可以用来分发任务。
FAN-IN模式:1个goroutine从多个通道读取数据,直到这些通道关闭。IN是一种收敛的模式,所以又被称为扇入,用来收集处理的结果。
// 附录3
在redis中,所有的键值对都会用内置的哈希表保存在内存里??? TODO
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中挪出来
技术群讨论,面试题
A: 我记得可以申请无限制的 先申请无限的,然后缺页时加载到物理内存,如果物理内存满了,那就交换到磁盘里面去?
最多申请: 磁盘+物理内存 那么多?
B: 进程只能申请虚拟内存
C: 不,就是问实际物理,我回答虚拟内存,寻址范围,动态段页分配(就是虚拟内存到物理内存的映射)这些点都被面试官驳回了. [抠鼻]搞不懂他到底想问什么
TODO
// 附录1
// 附录1
1.15 | 基础篇:Linux内存是怎么工作的?虚拟地址(内核空间和用户空间)+缺页异常+物理内存 2.一个程序最多可以使用多少内存?-操作系统还要占用内存 3.Linux 内存相关问题汇总-malloc申请内存的流程-TODO
技术群讨论,面试题
Q: 提问:一个存储系统,包括缓存和持久化存储,用DDD的思想落地,提供接口给外部调用,注意事项:有可能会更换存储引擎。
Q: 提问:实现一下ab之间的扫码支付功能。安全,流量,一致性等问题都要考虑。
Q: 提问:实现一个定时回调系统,外部接口提供注册回调事件功能,数据规模一天500w,需要按照用户注册的时间,比如未来30分钟,后天0点,等,类似延时队列。
A: 类似我们内部做的生产,消费者服务. 调用注册回调接口,我们来调用回调接口,完成消息的消费.
我负责电商的商品搜索,经常遇到这样的需求:当商品参加某个活动或满足某个条件时,希望可以在用户搜索时进行加权,曝光更靠前.
比较常用的实现思路: 1.往es写入1个字段标记是否满足某个条件 2.搜索时对这个字段,进行查询时权重调整. 2.1 调整字段weight 2.2 将function_score查询与field_value_factor结合使用 // 附录2
// 附录2
1.我们希望将更受欢迎的博客搜索结果列表中相对较上的位置–在搜索时,可以将function_score查询与field_value_factor结合使用,即将点赞数与全文相关度评分结合: 2.实战|Elasticsearch自定义评分的N种方法-有没有办法让同一个索引里面对固定的查询返回的相关性评分是在固定的范围之内的?比如0-100分这样的?
公司的搜索项目中,为了性能,做了大量的内存缓存,这些缓存的更新使用了比较多的timer.
同时,印象中记得有次Go版本升级,使用了四顶堆提升timer性能.
基于这些原因,在复习数据结构-堆的应用时,可以做结合工作生产实践做一些发散.
假设我们有一个定时器,定时器中维护了很多定时任务,每个任务都设定了一个要触发执行的时间点
shell: 在远程主机上执行shell命令 HTTP: 执行HTTP-GET请求
Crontab每隔1分钟扫描任务列表,如果要运行就fork一个进程运行。// 这个任务可以叫创世任务(因为了解区块链的创世区块,所以这里突然想到了这个名字)
在分析粘包和半包问题时,对于Go net包下的Client的Read和Write方法的底层原理产生了一些疑问`,
// client.go 通过套接字发送消息
conn.Write([]byte(words))
// server.go 通过套接字读取消息
n, err := conn.Read(buffer)
// 附录1 从⽤户进程创建 socket,到⼀个⽹络包抵达⽹卡到被⽤户进程接收到,总体上的流程图如下:
// 附录2
1.本机: 理解了实现再谈网络性能.pdf-1.3 TCP下用户进程如何和内核协同? 2.本机: UNIX网络编程卷1:套接字联网API(第3版)-第4章 基本TCP套接字编程
调用第三方接口时发现的报错
日志中错误:net/http: request canceled (Client.Timeout exceeded while reading body)
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.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 画一些交互图,时序图 来记录动态运行的流程.
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的消息。
Name Server提供的最重要的功能就是Topic路由信息的维护,任何发送消费的消息都归属于某个Topic,发送消费都需要和Broker打交道。Name Server维护了Topic/Broker的关联关系。
Topic/Broker之间的关系则是Broker定期上报Name Server,Name Server自身定期检查Broker信息是否有效,如果Broker宕机或下线,对应的路由信息会被移除。
Name Server中用于维护相关信息的代码实现主要位于RouteInfoManager,其关键用于记录的成员为,
A: 简单总结
2.微服务(broker)通过发送心跳包刷新自身的存活状态. 如果微服务(broker)挂了,那么就将这个微服务(broker)从注册中心摘掉。
研究一下RPC的底层原理的过程,遇到TCP粘包问题(粘包这个词不对,会让人误解)
怎么解决TCP网络传输「粘包」问题?-首先,什么叫“包”? 在基于tcp开发应用的语境下,其实有两种“包”,其一是tcp在传输的时候封装的报文,分为包头和负载,其二是应用开发者在应用层封装的报文结构。
看了一篇自己动手写RPC框架的博客,觉得通过造轮子来学习的方式很好。
1.进程间通信的方式 2.为什么
// 附录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的基本单位都是字节数组,所以必须转换为字节数组。
A: 把网络字节数组或持久存储中的字节数组,还原为内存中对象的过程.
// 附录2 A:
序列化是为网络传输和持久化存储,而这两者的方式都是IO,IO支持的数据格式只有字节数组. 所以当要传输或存储内存对象时,都需要把内存对象转换字节数组。
对象转成字节数组的时候就制定一种规则
假设,现在你有1个进程
Go的几种RPC实现
RPC通信过程基本流程: 序列化-编码-解码-反序列化
RPC通信过程代码流程
1.What is Serialization? 2.为什么要序列化? 3.Go net/rpc 包的深度解读和学习 4.这里有一篇,Java系列的RPC框架,是基于Netty搭建网络模块的: 一起写RPC框架(三)RPC网络模块的搭建一 网络传输模型
最开始是沟通,状态管理的技术方案,他给了我一个博客,是状态机的简单实现。
过了几天,他加我微信,问我用得咋样。
然后谈到了跳槽和团队管理问题,我问他: code review有没有啥经验总结的文章[奸笑],他发给我下面这个文章
线上一个定时任务,使用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.基准测试验证不同配置对性能的影响
2.MaxIdleConns应该始终小于或等于MaxOpenConns.
Q:空闲连接是否可以设置得很大 TODO
A:
空闲连接过多的坏处 1.空闲连接占用内存 2.如果一个连接空闲太久,那么它也可能会变得不可用。 因此,将MaxIdleConns设置得太高实际上可能会导致连接变得不可用,并且使用的资源比使用较小的空闲连接池(使用的连接更少,使用频率更高)的情况下要多。 所以只有你很可能马上再次使用这些连接,你才会保持这些连接空闲。
// 附录2
var DefaultConfig = map[string]string{
"charset": "utf8",
"parsetime": "True",
"loc": "Local",
"timeout": "15s",
"readTimeout": "2s",
"writeTimeout": "5s",
"maxIdle": "10",
"maxOpenConn": "100",
"maxLifetime": "1",
}
Q13 如何判断 2 个字符串切片(slice) 是相等的?
针对这个中的b = b[:len(a)],使用go 1.15.6编译,已经没有效果了,在len判断的时候就已经做了优化
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
主要看了这篇文章里面的一句话:为了解决TCP无限创建与TCP频繁创建销毁带来的资源耗尽、性能下降问题。 MySQL服务器里有专门的TCP连接池限制接数,采用长连接模式复用TCP连接,来解决上述问题。
//附录1 那么单条 SQL 执行时间是多少呢?我们统计了一段时间的 SQL 执行时间,发现 SQL 的平均执行时间大概是 1ms,也就是说相比于 SQL 的执行,MySQL 建立连接的过程是比较耗时的。
这在请求量小的时候其实影响不大,因为无论是建立连接还是执行 SQL,耗时都是毫秒级别的。
可是请求量上来之后,如果按照原来的方式建立一次连接只执行一条 SQL 的话,1s 只能执行 200 次数据库的查询,而数据库建立连接的时间占了其中 4/5。
A: 那么为什么频繁创建连接会造成响应时间慢呢?来看一个实际的测试。
golang server epoll client 使用连接池 15万+ qps
// 附录2
1.07丨池化技术:如何减少频繁创建数据库连接的性能损耗? 2.golang http服务器连接重用_系统优化连接池技术原理与实现 -如何设计一个连接池 本文例子主要讲解golang数据库组件database/sql连接池的实现
这个服务对应下图中的广告主server
的接口一,一般业内叫监测链接,是一种回调链接,用于用户点击广告,媒体平台通过这个接口通知给我们这样的广告主。
这个业务之前使用php写的,因为初创,工期紧,当时编码人员也是首次接触该业务,对业务特点理解不深入等原因。
导致旧系统具有以下特点:
1.旧系统是纯粹的过程式编码方式,每对接一个新的渠道,就要完全新开发一个渠道代码。
2.在阅读旧代码的过程中,可以很明细的意识到所有渠道的业务处理流程都都是相似的N个步骤,不同渠道间的变化点在于:具体的每个步骤细节有差异。
3.整个业务处理的输入是不同, 数据处理的步骤是相似的, 输出都是构成1个Dao结构体,然后把Dao写表。
经过梳理和分析,我们找到一些变与不变的东西:
1.不同渠道业务具有相似的部分:处理步骤的相似,并且不同步骤没有必然的顺序依赖。
2.不同渠道的变化点在于每个步骤的具体细节不同。
3.所有渠道的输出都是相同的:构造1个Dao结构体。
便于维护
1.对接新的广告渠道时,尽可能的能复用,只对接每个步骤的细节部分,主体的步骤顺序不变。
2.同事之间交接服务的时候,也更方便同事迅速看出主要设计,get核心业务点, 明确变化点。
通过接口来实现约束相似的步骤
通过接口方法的不同实现,来应对步骤内部具体细节的变化
这样设计,是从业务出发,而不是为了硬套设计模式。
设计完成之后,发现这个设计基本就是代理模式,于是调整一些命名,显示的体现这个模式在项目中的应用。
1.是结合业务特点,找到其中不变和变的部分,然后想办法用接口来承接这个不变的部分,用多态来封装变化的部分。
2.不要用设计模式去套业务,生搬硬套的做法不利于提高自身的软件设计能力。偶尔碰对了,也只是运气。逐步推理出的设计会更好,哪怕这个设计没有名字,也是好的设计。
3.适当考虑和同事讨论设计。 一方面,讨论设计是一个让人愉快,也有意思的事情。另一方面,我最开始和leader讨论,在讨论的过程可以逐渐明确变与不变的东西,同时也能保证目标和leader偏差不大。
TODO
TODO
非常尴尬,经过和同事讨论,这个模式实际上是模板方法,而不是代理模式。 不过具体叫什么模式不是主要的,重要的是,能够识别出变和不变的地方,并知道如何利用接口,多态等编程元素来提取不变的东西和封装变化的东西
另外整个项目设计还进行的一些改变。
因为分层之后,什么逻辑应该放哪一层,没有梳理得很清楚,导致一些不太好的地方。
这个之后我再梳理一下。// TODO
我司成立于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实现,配合业务部门逐渐实现迁移。
在以上工作过程中,我深刻的意思到软件设计的必要性,同时也意识到自己对软件原则和设计模式的应用还比较初级,未能达到得心应手的程度。
为了可以提升自己在应对复杂业务需求时的设计能力,最近在阅读<编程的逻辑-如何用面向对象方法实现复杂业务需求>,同时也在重构部分核心业务, 希望可以结合自身重构经历,再次理解一些常见设计模式的原理,熟悉其应用场景。编程的逻辑-如何用面向对象方法实现复杂业务需求>
3.<编程的逻辑-如何用面向对象方法实现复杂业务需求>编程的逻辑-如何用面向对象方法实现复杂业务需求>
4.<Head First设计模式-第3章 装饰对象装饰者模式>
本章可以称为"给爱用继承的人一个全新的设计眼界"。
我们即将再度探讨典型的继承滥用
问题。你将在本章学到如何使用对象组合的方式,做到在运行时装饰类。
为什么呢? 一旦,你熟悉了装饰的技巧,你将能够在不修改任何底层代码的情况下,给你的(或别人的)对象赋予新的职责。
关于web框架 自动绑定和校验 表单参数,在之前的几家公司内部自研web框架都有见过,包括Java和Go的自研框架。
最近使用gin搭建广告服务,也涉及了相关知识点,这里记录一下绑定原理。
Go框架中,主要是依靠解析结构体中的tag进行匹配。
其中主要的难点是:
1.Go是静态语言,在判断字段类型时,会有很多分支进行判断。
2.对结构体的指针和非指针赋值逻辑不同。
3.POST请求发送的body如果是嵌套的json内容,映射到对应的嵌套结构中需要使用递归算法,实现一个生产级的夹杂着业务的递归算法还是比较繁杂的。
PS.本文均未谈及性能问题
技术讨论群取群友分享的代码阅读的另类工具
1.此工具的目的是使用来自调用图的数据及其与包和类型的关系,为开发人员提供 Go 程序的可视化概览。 这在代码复杂度更高的大型项目中特别有用,或者当您只是试图理解其他人的代码时。
我所在的公司是一个二手电商公司,所有即涉及买家,也涉及卖家,所有商品状态流转也就涉及两条主线(买家&卖家),并且因为历史原因,这两条主线还有一定的交织和关联。
其中,买家侧的商品状态比较少,流转情况也较少,出现问题也较少,本篇文章不深入分析,只有和卖家侧商品状态有交织时会提及。
文章主要分析卖家侧的商品状态流转。
卖家侧的商品状态有很多,因为每一个商品处理环节都需要定义状态,甚至还有不少状态有子状态,所有商品管理平台和卖家侧展示,这两块业务,常常出现问题,并且每次解决都需要重新梳理相关状态,导致问题解决特别棘手。
切换的一个新的角度来看待商品状态流转的问题。
从不同的系统角色出发,划分买家端,卖家端和中间处理状态这三种状态.
其中买家端状态不复杂,只需要一级状态即可表示. 卖家端和中间处理状态较为复杂,都才有二级状态进行表示。 所以一共是1+2+2=5个状态表示全部状态,这5个字段主要用于查询和筛选某些状态的商品列表。
这5个状态字段维护的则放在不同的业务逻辑。这样就把状态查询和状态管理分离开来。
简化了查询业务的代码,但是状态管理业务的代码应该是没有很多改进。
本次会议成果要点基本如上。
我个人认为附录2的有限状态机和状态转换表是必须要梳理的东西,但是精力比较有限,想法也没有很成熟,所以暂时没有在会议上提出。
最近公司多个技术领导,准备用领取驱动来对后台管理系统进行重构。我保持悲观态度。 又想到了这个商品状态流程的问题,再一次调研状态机,发现附录3的文章提到了状态机的一些应用。 同时我也梳理了商品状态流转的主要问题:
1.从体验角度看电商前端订单状态流转与后台联动-三、如何绘制订单状态流转
2.1深入浅出理解有限状态机
今天遇到一个需求
我: 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这部分的。
前缀树代码有的难,一时不好看,尤其是通配符:
和*
的处理
最近接手一个广告业务,包括广告下,广告渠道归因,广告上报,其中主要的难点在广告渠道归因,有很多坑,这里做一些记录。
2.归因分析(Attribution Analysis)模型解析
3.多渠道推广场景下,如何实现App用户增长的精准归因?– HTML5的渠道又无法获取用户的设备号信息,所以这次行为很可能就会被归因在优先级较高的信息流形式下,导致误差的产生
4.广告监测平台(二):详解渠道归因: 1.渠道归因的核心:跟踪链接 2.第一,向应用(APP/H5/WEB)提供链接区分广告渠道 3.归因回溯窗口
netstat -tunlp | grep 1111
lsof –i: 端口号
// 附录1 ss -ltnp |grep 端口号
# -l 表示只显示监听套接字
# -t 表示只显示 TCP 套接字
# -n 表示显示数字地址和端口(而不是名字)
# -p 表示显示进程信息
netstat 和 ss 的输出也是类似的,都展示了套接字的状态、接收队列、发送队列、本地地址、远端地址、进程 PID 和进程名称等。
查找进程 ps -ef | grep tomcat
根据进程找端口 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来查文件或目录被谁占用
1.执行命令:pwdx PID 命令,就得到进程jar包所在的位置
2.执行命令:ll /proc/PID/cwd 命令,也能得到进程jar包所在的位置
1.34 | 关于 Linux 网络,你必须知道这些(下) 2.linux查文件被哪些程序占用—fuser和lsof的使用 3.lsof命令使用场景 4.fuser=使用文件或文件结构识别进程 5.Linux下根据进程ID查看进程文件的路径
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;
数据库 | 表名 | 记录数 | 数据容量(MB) | 索引容量(MB) |
---|---|---|---|---|
xxx | t | 343112190 | 39595.23 | 22068.64 |
-- 主要存放用户领取和使用优惠券的数据
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;
略 2021-08-11更新: TODO 从Java生态中了解到了一些组件,然后在Go生态中查找了相关资料. 分库分表实践-Sharding-JDBC mysql sharding 官方_GitHub - endink/go-sharding: Mysql 分库分表中间件 小米开源数据库中间件Gaea实战(有彩蛋)
// 附录11 我在公司内部一直奉承的重构思路:步子大了小心扯着蛋.
1.搜集sql,得出分库,分表或分区键 2.如果要水平分表,需要寻找自增id的替代。这种情况可以先去掉数据库中自增ID,为分片和后面的数据迁移工作提前做准备。 // 小步前进 3.双写 4.mysql或polardb-x
分库分表会导致多个业务线和统计业务的编码复杂化,所以不考虑分库分表,采用一种类似冷热分离的思路: 使用2个表。
1个热点数据表(阿里云rds),给业务用。单表总量基本可以维持在一亿以内,每几个月清理一次数据,性能尚好。
1个全量表(阿里云adb),给统计用。单表一年新增10亿以内,性能还好。
每n天通过脚本将一段时间内的热点数据表的数据(insert|update|delete
),增量的同步到全量表。
有了方案2,暂时不考虑这个。原理可参考附录2。
方案2
待我搞完才知道,应该能支持个3-5年。
尽量避免分库分表,采用冷热分离的思路
发到一些技术交流群中,收到如下反馈
A(我): 确实有一部分冷数据一直保留在热点表里面的,就是那种只能领取一次的券,需要校验用户只能领取一次。 除此之外,只有统计需要查询其他的冷数据,业务上不需要查。 所以删除的冷数据仅仅指的是,业务上不再需要查询的那部分冷数据,这部分数据是大头。 业务上需要查询的冷数据,则保留在热点表中。
A(我): 估计分库分表我要重新考虑下,在多个群被质疑了
A(其他): 分库分表分场景,优惠券适合冷备吧
A(其他): 哦,有大量的优惠券没有领取
A(我): 恩,所以很多都没意义了,没有领取又失效了,那么业务上就没意义了,只有统计上需要。
A(我): 还在调研,暂未考虑。
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 业务增长,数据量增长,准备做一些分库分表的事情
// 附录8,9 mysql单索引下二级索引的原理:
分布式二级索引原理:
阿里云的DMS,无锁变更执行DDL语句
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.MySQL查看数据库表容量大小-3.查看指定数据库容量大小
2.首页 > PolarDB-X 云原生分布式数据库 > 技术白皮书 > 技术原理
3.MySQL8.0的information_schema.tables信息不准确怎么办
4.mysql - information_schema.TABLES.DATA_FREE 在 MySQL 中是什么意思?
5.MySQL表碎片化(Table Fragmentation)的原因
9.PolarDB-X什么样的技术,才能让你“忘掉”分区键这个东西呢。-二级索引与分区键
11.分库分表带来的问题-我们的系统真的需要分库分表吗?-这种情况可以先去掉数据库中自增ID,为分片和后面的数据迁移工作提前做准备
一早上来到公司,看到报警有大量报警信息。
云服务器实例ID=xxxx,device=/dev/vda1] /dev/vda1 ,磁盘使用率(100%>=97% )(100%>=97%),持续时间5小时46分钟
这个服务主要有2个功能 1.作为定时任务的主要执行机器。 2.部署了archery这个sql审核平台。
一开始怀疑是某个定时任务产生了大量日志文件,问了周围同事,最近并没有新增定时任务。
靠经验判断不行,那只能去定位大文件的位置,然后再继续分析问题了。
可以发现是/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
du -s /* | sort -nr
命令查看那个目录占用空间大因为没有权限,所以我这里给出示意结果。
/data/aplum/Archery-1.7.10/src/docker-compose 占用很大
du -lh –max-depth=1 | sort -n |
du -s 怀疑的路径 | sort -nr
一层层排查,找到占用文件多的地方。我今天发现的问题是/data/aplum/Archery-1.7.10/src/docker-compose/mysql/datadir/
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: 略
DELETE FROM 某个大表 where user_id = 0 and id BETWEEN 0 and 5000000;
2021-07-08 04:30:00
左右,云盘使用空间从75%猛增为100%。
这个sql执行时间为2021-07-08 04:05:00
,正好接近磁盘分区(/dev/vda1)的使用空间陡增的时间,/dev/vdb1
这个大磁盘中因为这是archery的docker集群的一个挂载文件,所以只能把archer整体迁移。
正好这个机器还有1个空间充足的磁盘/dev/vdb1
,mv过去即可。
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 # 它尝试自动完成包括构建镜像,(重新)创建服务,启动服务,并关联服务相关容器的一些列操作。链接的服务都将会被自动启动,除非已经处于运行状态。多数情况下我们可以直接通过该命令来启动一个项目。
// 附录5 archery每执行一个sql,就会在备份库里生成对应的sql语句,而且产生的回滚语句都是针对一行的, 所以今天的大表的删除语句产生的回滚sql应是2千多万行,导致磁盘占用从75G变为100G,基本对得上。
统计当前文件夹(目录)大小,并按文件大小排序: du -sh * | sort -n du -lh –max-depth=1 |sort -n
// 附录7,8
背景
也是磁盘报警,然后我们发现是日志切割问题导致产生了一个超大的日志文件。
然后我们删除了这个文件,但是df -lh查看的Avail空间并没有减少 和 du -sh *
去日志目录统计磁盘占用, 发现并没有减少。
TODO 这里好像可以说明磁盘文件并没有被释放。需要理解磁盘原理后明确这点。
现象 rm删除,但是发现并没有释放磁盘空间。需要重启进程才能真的释放磁盘空间
通过lsof命令查找运行中的进程占用已经删除的文件:
lsof | grep delete |sort -nrk 7|more
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';
为什么需要字符串前缀索引
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
昨天定位OOM的时候,有重启ECS服务器,重启之后,发现需要做一些基础准备工作,主要是几个agent程序。
1.手动重启go-cron的agent // 定时任务调度框架
命令:
2.【已经添加开启启动项】自动部署需要运维手动重启/home/staragent/bin/staragentctl ,root权限 // 用于阿里云效流水线
3.【已经添加开启启动项】consul的agent需要手动重启 // consul服务注册发现的agent
命令:
// 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)
}
}
一时找不到原因,让运维同事重启了服务器,先解决问题再定位原因。
先怀疑cpu,磁盘问题,同事们回想一下最近的修改,主要是消息中心和接入阿里云的日志服务。
浏览了最近提交的一些服务的代码,没有看出异常。
重启服务器后,使用vmstat、pidstat 等性能工具,观察系统和进程的 CPU 使用率。 // 附录1
# 所有进程的 CPU 统计信息
pidstat -u 1 10
# 显示 IO 统计信息
pidstat -d
# 显示内存统计信息
pidstat -r 10 100 > pid_mem.lgo &
发现消息中心服务的内存占用不断的缓慢增加
发现同一时间服务收到SIGKILL信号的重启
重启服务器和服务后,获取内存的pprof文件 //附录2
指标中包含了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命令结合源代码来分析情况。
因为已经找到原因了,我也就没有继续这么搞。
最近升级公司的canal集群,同时也了解canal集群的基本交互流程和HA的实现方法。
为了保证有序性,一份 instance 同一时间只能由一个 canal client 进行get/ack/rollback操作,否则客户端接收无法保证有序。
server代表一个canal运行实例,相当于一个jvm
instance相当于一个数据队列,即一个mysql实例 (1个server可以对应多个instance)
canal的ha分为两部分,canal server和canal client分别有对应的ha实现
[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]
canal.ip =
canal.register.ip = canal.port = 11111 #canal server提供socket tcp服务的端口 #canal-server接收zk通知的端口吗? 这个端口谁来访问? canal.metrics.pull.port = 11112 #c-s暴露的监控数据抓取接口. 可以配置到prometheus.yml的targets里面
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.13 升级到 go1.16,其中一个同事提到了这个博客,发现我以前也看过这个问题,再次记录以下
https://github.com/golang/go/issues/40999
Q:从内聚性的角度思考为什么要有ProductInfo对象,这个对象应该有哪些属性和行为(方法),为什么这些属性和方法应该放在这一个类中? // 对象+交互
r, err = (&models.ProductInfo{
ProductId: product_id,
UserId: user_id,
TmNow: tm_now,
IsFirstOrder: firstOrder}).
GetProductInfo(models.DetailFormat)
而 Canal 的设计初衷就是为了解决上述问题,故我们可以边思考这个场景,来反推一下 Canal Instance 的设计理念。
https://zhuanlan.zhihu.com/p/345736518
比如可以将instance.properties的公用参数,抽取放置到这里,这样每个instance启动的时候就可以共享.
最近在搭建一套Canal同步环境,又遇到了一些问题,这里参考附录6{Canal v1.1.4版本避坑指南}做一些记录.
报错: Caused by: com.alibaba.otter.canal.parse.exception.CanalParseException: column size is not match for table:数据库名称.表名称,新表结构的字段数量 vs 缓存表结构的字段数量;
原因: 表结构缓存
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的意思,希望能帮助后来者.
一、控制同步起始位置
预发布环境RocketMQ下,canal在A1时间点启动,然后A2时间点停止;
经过一段时间后,B1时间点再次启动canal服务:此时canal会先同步A2到B1时间段之间的数据,
但是希望直接从B1(2020-10-14 14:00:00)时间点开始同步数据。
删除zk中的位点信息 rmr /otter/canal/destinations/dev02db
启动instance
对canal有了一定的线上维护经验,准备做一次分享,所以进一步学习一下架构设计。
A:
// 附录7
2.开源实战 | Canal生产环境常见问题总结与分析 TODO 3.一文详解 Canal Instance 设计理念与定制开发思路-Instance 是 Canal 数据同步的核心,在一个 Canal 实例中只有启动 Instace,才能实现数据的同步
有的问题在分阶段起关键作用,有的问题在合阶段起关键作用(比如归并排序,分阶段并没有得到解决,合并阶段完成才得到排序结果)
一些比较形象的分治思想的应用。
分治思想,对应的编程实现方式一般是递归的。
排序的分治思路:每次处理后,一个或多个树被放在了最终的位置.(排序后的应该在的位置)。或者一个或多个数的相对位置被调整到了和最终的相对位置一致。//如果所有数的相对位置一致,那么这个数组自然就有序了。
递归的编码方式:树形处理顺序,压栈,压栈…,弹栈,弹栈, // 分 [] | A类问题 [] / \ 问题规模更小的A类问题 [] /\ /\ 问题规模再小的A类问题 // 治: 有时合就是治的一种方法.
快速排序: 每次把一个数放在合适的位置,在分的阶段解决问题?
归并排序: 每次把多个数的相对位置调整正确,在合的阶段解决问题? 注意合并之后这些数也没有在最终应该在的位置,只是相对位置已经是最终的相对位置了。
O(n) 时间复杂度内求无序数组中的第 K 大元素。
归并排序是否稳定排序? 归并排序的空间复杂度是多少? 归并排序为什么使用不广泛,有啥缺陷?
O(n2)的排序算法 | 思路要点 | 为什么稳定或者不稳定 | |
---|---|---|---|
冒泡排序 | 每次冒泡把最大 的数未排序数组的最后 |
unsorted+sorted(max) | 都是相邻交换,没有跨越式交换,发现相等时可以不移动 |
插入排序 | 反向遍历前面的排序部分,腾出位置,插入第一个未排序元素 | sorted+unsorted | 插入时可以插入相同元素的后面 |
选择排序 | 选择未排序区间最小 的元素,交换到已排序区间的末尾 |
sorted+unsorted | 交换时可能跨过和自己相等的数,破坏稳定性 |
可以看出,冒泡排序和选择排序,都是把数组分为未排序和排序的两部分,然后从未排序部分找出最值放入到已排序部分。
计划如下:
1.首先了解数据结构 1.1 为什么选择B+tree. 从BST,B-tree,B+tee的演进,同时讨论为什么不选跳表,LSM
2.死锁分析之insert
3.幻读和间隙锁
查找thrift文件位置 /opt/xxxx/thrift/bin/thrift
新同事,修改.bashrc文件, 把/opt/xxxx/thrift/bin/thrift加入到环境变量中
注意设置完环境变量,需要使用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
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】
3.Percona-Toolkit系列之pt-kill杀会话利器
5.innodb_undo_tablespaces的含义。undolog表空间清理 执行一个长事务,可以肉眼观察到undolog日志文件的膨胀。
1.insert操作会加X锁
Q:为什么insert要加X锁 A:这里的x锁是行锁,如果不加,那岂不是能插入两条id为6的值
2.insert操作如果发生重复唯一键冲突,则不会加X锁,而是加S锁
这里理解错误了: select * from information_schema.innodb_locks;里面获取的是准备获取但是还没有获取到的哪些锁.
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) |
只有在特殊情况下,才会将隐式锁转换为显示锁。
这个转换动作并不是加隐式锁的线程自发去做的,而是其他存在行数据冲突的线程去做的。
例如事务1插入记录且未提交,此时事务2尝试对该记录加锁,那么事务2必须先判断记录上保存的事务id是否活跃,如果活跃则帮助事务1建立一个锁对象,而事务2自身进入等待事务1的状态,可以参考如下例子:
INNODB_LOCKS表主要包含了InnoDB事务锁的具体情况,包括事务正在申请加的锁和导致其他事务阻塞的锁。 不是已经获取到的锁。
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的使用,没有遵循订阅关系一致性,导致丢消息。// 参考附录
线上: 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
现象入手,
经过抓包确认了是自连接创建3s后,http客户端主动断开连接(怀疑超时原因),
然后进一步跟踪源码,同时按照调用链路贴出源码,追踪出了超时原因和超时错误产生的位置.
整个过程环环相扣,读完印象深刻,好像自己分析定位出来的原因一样.
从这篇文章可以学会如下技能:
1.抓包
3.net/http包复用长连接的缘由
i/o timeout
在源码中产生的位置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包来确认。
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
那还加毛
我在公司内部进行的分享:2021 618大促优化记录.pdf
今天看到一篇性能优化博客(附录1),其中有几点可以参考
1.辅助工具的使用 2.编写benchmark单独验证。//我之前性能优化只看pprof的结果和最终监控的响应时间,验证得还不够充分。 3.string转[]byte的黑科技 4.需要对redis原理有一定了解,才能知道redis hash需要取多次slot。// TODO 准备学习一些redis数据结构原理
公司的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。
一般而言只有升级go版本才能提升gc的能力。
同一个go版本,垃圾更少,gc次数更少, 提高的是吞吐量,而不是gc的能力,垃圾收集器清理垃圾的能力并没有提高。
比如: 10w次请求,产生垃圾w。优化后。20w次请求,才产生垃圾w。
通常情况下 用–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方法
我负责的搜索服务,每次大促前都会进行一些性能优化. 今天开启mysql的PreparedStatement,p95直接从200ms提升到了100ms.吓死我了。
性能优化的惊喜-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的话没排序会产生几条 默认事务关掉性能应该还可以提升
preparestatement是基于session做存储,如果session读了,就会有较多的重复preparestatement
mysql抓包能看到先发包过去编译sql
云数据库RDS的MySQL版无法查询performance_schema值
//附录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
今天升级mysql8。依然出现了如下错误:
"error":"Error 1461: Can't create more than max_prepared_stmt_count statements (current value: 16382)"
查看阿里云后台主库配置是49146,从库配置还是16382。
问了下运维:只改了主库,忘了改从库了。
设置从库参数即可。
Com_stmt_prepare 减去 Com_stmt_close 大于 max_prepared_stmt_count 就会出现这种错误。那么我们手动调高max_prepared_stmt_count(取值范围:0 - 1048576,默认16382)即可解决
1.1请确认Com_stmt_close的值是否接近于Com_stmt_prepare – 一个可能的原因是应用端那边没有关闭prepared的语句
MySQL · 特性分析 · MySQL的预编译功能 TODO
返回的结果可能会有重复,需要客户端去重复,这点非常重要;
那我们如何去遍历大数据量呢?我们可以采用redis的另一个命令scan。我们看一下scan的特点
复杂度虽然也是 O(n),但是它是通过游标分步进行的,不会阻塞线程
提供 count 参数,不是结果数量,是redis单次遍历字典槽位数量(约等于)
同 keys 一样,它也提供模式匹配功能;
服务器不需要为游标保存状态,游标的唯一状态就是 scan 返回给客户端的游标整数;
返回的结果可能会有重复,需要客户端去重复,这点非常重要;
单次返回的结果是空的并不意味着遍历结束,而要看返回的游标值是否为零
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
}
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]
}
在已有切片的基础上进行切片,不会创建新的底层数组。因为原来的底层数组没有发生变化,内存会一直占用,直到没有变量引用该数组。 因此很可能出现这么一种情况,原切片由大量的元素构成,但是我们在原切片的基础上切片,虽然只使用了很小一段,但底层数组在内存中仍然占据了大量空间,得不到释放。 比较推荐的做法,使用 copy 替代 re-slice。
针对查询偏移量的测试:
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万以后,查询时间急剧增加。
这种分页查询方式会从数据库第一条记录开始扫描,所以越往后,查询速度越慢,而且查询的数据越多,也会拖慢总查询速度。
子查询
A:内存没有释放。清空只是修改了一个标记,底层内存还是被占用了
A: map = nil 这之后坐等垃圾回收器回收就好了。
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
如果你用map做缓存,而每次更新只是部分更新,更新的 key 如果偏差比较大,有可能会有内存逐渐增长而不释放的问题。要注意。 或者map
我们可以在遍历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
TODO
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)
}
执行结果
原因 卧槽,说明main携程执行太快了。。。 因为就1个cpu啊,只能执行一个内核线程。 一个内核线程(GPM的M)某个时刻只能执行一个携程。 子携程还没来得及启动,main就完事退出了。
首先,系统的理解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
// 附录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.1为何这段go程序不能让出控制权? - 月明星稀的回答 - 知乎
2.Go: Goroutine and Preemption
3.关于 Go1.14,你一定想知道的性能提升与新特性-1.2 goroutine 支持异步抢占
做业务需求时,发现有主键排序的需求,但是排序可能导致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.不同隔离级别下的乐观锁有区别吗?(可重复读和读已提交) 2.当前读和锁等待的必然联系 3.当前读有哪些? 4.当前读,总是读取已经提交完成的最新版本。
因为当前读必须要读最新版本,如果此时该行数据上有事物尚未提交,则当前事务必须等待锁,这样才能保证读取到最新的记录.
Q: 事务隔离级别是可重复读。现在,我要把所有“字段 c 和 id 值相等的行”的 c 值清零,但是却发现了一个“诡异”的、改不掉的情况
我看leader把select和udpate放一个事务里,这个乐观锁有点尴尬。
如果是可重复读的隔离级别,那么对当前事务来说,只有version = version的情况啊,这个是横成立的。 即使其他事务对version进行了修改,这个事务也是看不到的吧?
A: 发现可重复读的情况下, 乐观锁也是没问题。因为update是当前读。乐观锁在可重复读下的使用
// 附录1
Q:什么业务要加锁,加乐观锁 or 悲观锁
Q:乐观锁怎么加
Q:悲观锁怎么加
乐观锁在可重复读的隔离级别下,每次重试都是快照读,永远不会更新成功,很多请求一直重试,则会导致事务锁等待超时。
update操作是当前,会加行锁,导致修改同一行的数据锁等待,多等几次就超时了。
// 附录4
解决方案: 乐观锁需要配合读已提交的隔离级别才可以,这样是当前读,可以在最新版本的基础上进行更新,才会成功。
// 附录3
秒杀等高并发场景下,每个版本只能成功1个,也就是每次只能有1个人抢购成功。 比如有库存100个,每人买1个,那么单个用户最多需要重试99次才能买到,但是正常是用户只需要抢购1次,如果有库存就能成功。 而不是有库存但是版本号不对则不能成功.
TODO Q:什么场景是必须在正确的版本号基础上修改才可以.
Q:什么场景不依赖版本号,只依赖某些限制条件(如:库存),则可以修改.
比如,库存模型和份额+-
1.08 | 事务到底是隔离的还是不隔离的? 2.本机资料【169期】面试官:同学,分析一下MySQL_InnoDB的加锁过程吧-技术圈.pdf 3.使用mysql乐观锁解决并发问题 4.乐观锁加重试,并发更新数据库一条记录导致:Lock wait timeout exceeded
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`)
);
https://talkgo.org/t/topic/80
Micro是面向开发人员的微服务框架,如果人们用了K8s,那应该就不要用micro。
// 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/
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
}
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.
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和2的博客做,因为这两个博客已经写得很完善了,我就不在赘述了。
1.相对路径访问仓库的图片
![_config.yml](/images/jekyll-logo2.png)
效果:
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>
具体步骤完全参考附录3,直接生效.
两点注意:
1.需要新注册一个github小号,在小号里注册oauth application,使用小号app的client secret.
2.生成github oauth的”Authorization callback URL”,填自己博客路径即可,比如我填写的https://yudidi.github.io/
第一个成功的评论: https://yudidi.github.io/pprof-heap-alloc/
// 附录5,6
4.本篇博客的源文件在这里,遇到问题可以参考我的源文件和历史提交记录,都是透明开源的
6.jekyll如何写一个导航栏? 搭建导航遇到一些问题,需要更多的理解Jekyll
7.搭建一个免费的,无限流量的Blog—-github Pages和Jekyll入门
祭出大绝招.搞了半天没搞出来,切换思路. 参考附录7的末尾,提到了
至此,最简单的Blog就算搭建完成了。进一步的完善,请参考Jekyll创始人的示例库,以及其他用Jekyll搭建的blog
所以我准备,参考一个有导航和评论的现成博客,源码全部复用即可.