美團在Redis上踩過的一些坑-2.bgrewriteaof問題
一、背景
1. AOF:
Redis的AOF機制有點類似於Mysql binlog,是Redis的提供的一種持久化方式(另一種是RDB),它會將所有的寫命令按照一定頻率(no, always, every seconds)寫入到日誌檔案中,當Redis停機重啟後恢復資料庫。
2. AOF重寫:
(1) 隨著AOF檔案越來越大,裡面會有大部分是重複命令或者可以合併的命令(100次incr = set key 100)
(2) 重寫的好處:減少AOF日誌尺寸,減少記憶體佔用,加快資料庫恢復時間。
二、單機多例項可能存在Swap和OOM的隱患:
由於Redis的單執行緒模型,理論上每個redis例項只會用到一個CPU, 也就是說可以在一臺多核的伺服器上部署多個例項(實際就是這麼做的)。但是Redis的AOF重寫是通過fork出一個Redis程序來實現的,所以有經驗的Redis開發和運維人員會告訴你,在一臺伺服器上要預留一半的記憶體(防止出現AOF重寫集中發生,出現swap和OOM)。
三、最佳實踐
1. meta資訊:作為一個redis雲系統,需要記錄各個維度的資料,比如:業務組、機器、例項、應用、負責人多個維度的資料,相信每個Redis的運維人員都應該有這樣的持久化資料(例如Mysql),一般來說還有一些運維介面,為自動化和運維提供依據
例如如下:
2. AOF的管理方式:
(1) 自動:讓每個redis決定是否做AOF重寫操作(根據auto-aof-rewrite-percentage和auto-aof-rewrite-min-size兩個引數):
(2) crontab: 定時任務,可能仍然會出現多個redis例項,屬於一種折中方案。
(3) remote集中式:
最終目標是一臺機器一個時刻,只有一個redis例項進行AOF重寫。
具體做法其實很簡單,以機器為單位,輪詢每個機器的例項,如果滿足條件就執行(比如currentSize和baseSize滿足什麼關係)bgrewriteaof命令。
期間可以監控發生時間、耗時、頻率、尺寸的前後變化
策略 | 優點 | 缺點 |
自動 | 無需開發 |
1. 有可能出現(無法預知)上面提到的Swap和OOM 2. 出了問題,處理起來其實更費時間。 |
AOF控制中心(remote集中式) |
1. 防止上面提到Swap和OOM。 2. 能夠收集更多的資料(aof重寫的發生時間、耗時、頻率、尺寸的前後變化),更加有利於運維和定位問題(是否有些機器的例項需要拆分)。 |
控制中心需要開發。 |
一臺機器輪詢執行bgRewriteAof程式碼示例:
Java程式碼- package com.sohu.cache.inspect.impl;
- import com.sohu.cache.alert.impl.BaseAlertService;
- import com.sohu.cache.entity.InstanceInfo;
- import com.sohu.cache.inspect.InspectParamEnum;
- import com.sohu.cache.inspect.Inspector;
- import com.sohu.cache.util.IdempotentConfirmer;
- import com.sohu.cache.util.TypeUtil;
- import org.apache.commons.collections.MapUtils;
- import org.apache.commons.lang.StringUtils;
- import redis.clients.jedis.Jedis;
- import java.util.Collections;
- import java.util.LinkedHashMap;
- import java.util.List;
- import java.util.Map;
- import java.util.concurrent.TimeUnit;
- public class RedisIsolationPersistenceInspector extends BaseAlertService implements Inspector {
- public static final int REDIS_DEFAULT_TIME = 5000;
- @Override
- public boolean inspect(Map<InspectParamEnum, Object> paramMap) {
- // 某臺機器和機器下所有redis例項
- final String host = MapUtils.getString(paramMap, InspectParamEnum.SPLIT_KEY);
- List<InstanceInfo> list = (List<InstanceInfo>) paramMap.get(InspectParamEnum.INSTANCE_LIST);
- // 遍歷所有的redis例項
- for (InstanceInfo info : list) {
- final int port = info.getPort();
- final int type = info.getType();
- int status = info.getStatus();
- // 非正常節點
- if (status != 1) {
- continue;
- }
- if (TypeUtil.isRedisDataType(type)) {
- Jedis jedis = new Jedis(host, port, REDIS_DEFAULT_TIME);
- try {
- // 從redis info中索取持久化資訊
- Map<String, String> persistenceMap = parseMap(jedis);
- if (persistenceMap.isEmpty()) {
- logger.error("{}:{} get persistenceMap failed", host, port);
- continue;
- }
- // 如果正在進行aof就不做任何操作,理論上要等待它完畢,否則
- if (!isAofEnabled(persistenceMap)) {
- continue;
- }
- // 上一次aof重寫後的尺寸和當前aof的尺寸
- long aofCurrentSize = MapUtils.getLongValue(persistenceMap, "aof_current_size");
- long aofBaseSize = MapUtils.getLongValue(persistenceMap, "aof_base_size");
- // 閥值大於60%
- long aofThresholdSize = (long) (aofBaseSize * 1.6);
- double percentage = getPercentage(aofCurrentSize, aofBaseSize);
- // 大於60%且超過60M
- if (aofCurrentSize >= aofThresholdSize && aofCurrentSize > (64 * 1024 * 1024)) {
- // bgRewriteAof 非同步操作。
- boolean isInvoke = invokeBgRewriteAof(jedis);
- if (!isInvoke) {
- logger.error("{}:{} invokeBgRewriteAof failed", host, port);
- continue;
- } else {
- logger.warn("{}:{} invokeBgRewriteAof started percentage={}", host, port, percentage);
- }
- // 等待Aof重寫成功(bgRewriteAof是非同步操作)
- while (true) {
- try {
- // before wait 1s
- TimeUnit.SECONDS.sleep(1);
- Map<String, String> loopMap = parseMap(jedis);
- Integer aofRewriteInProgress = MapUtils.getInteger(loopMap, "aof_rewrite_in_progress", null);
- if (aofRewriteInProgress == null) {
- logger.error("loop watch:{}:{} return failed", host, port);
- break;
- } else if (aofRewriteInProgress <= 0) {
- // bgrewriteaof Done
- logger.warn("{}:{} bgrewriteaof Done lastSize:{}Mb,currentSize:{}Mb", host, port,
- getMb(aofCurrentSize),
- getMb(MapUtils.getLongValue(loopMap, "aof_current_size")));
- break;
- } else {
- // wait 1s
- TimeUnit.SECONDS.sleep(1);
- }
- } catch (Exception e) {
- logger.error(e.getMessage(), e);
- }
- }
- } else {
- if (percentage > 50D) {
- long currentSize = getMb(aofCurrentSize);
- logger.info("checked {}:{} aof increase percentage:{}% currentSize:{}Mb", host, port,
- percentage, currentSize > 0 ? currentSize : "<1");
- }
- }
- } finally {
- jedis.close();
- }
- }
- }
- return true;
- }
- private long getMb(long bytes) {
- return (long) (bytes / 1024 / 1024);
- }
- private boolean isAofEnabled(Map<String, String> infoMap) {
- Integer aofEnabled = MapUtils.getInteger(infoMap, "aof_enabled", null);
- return aofEnabled != null && aofEnabled == 1;
- }
- private double getPercentage(long aofCurrentSize, long aofBaseSize) {
- if (aofBaseSize == 0) {
- return 0.0D;
- }
- String format = String.format("%.2f", (Double.valueOf(aofCurrentSize - aofBaseSize) * 100 / aofBaseSize));
- return Double.parseDouble(format);
- }
- private Map<String, String> parseMap(final Jedis jedis) {
- final StringBuilder builder = new StringBuilder();
- boolean isInfo = new IdempotentConfirmer() {
- @Override
- public boolean execute() {
- String persistenceInfo = null;
- try {
- persistenceInfo = jedis.info("Persistence");
- } catch (Exception e) {
- logger.warn(e.getMessage() + "-{}:{}", jedis.getClient().getHost(), jedis.getClient().getPort(),
- e.getMessage());
- }
- boolean isOk = StringUtils.isNotBlank(persistenceInfo);
- if (isOk) {
- builder.append(persistenceInfo);
- }
- return isOk;
- }
- }.run();
- if (!isInfo) {
- logger.error("{}:{} info Persistence failed", jedis.getClient().getHost(), jedis.getClient().getPort());
- return Collections.emptyMap();
- }
- String persistenceInfo = builder.toString();
- if (StringUtils.isBlank(persistenceInfo)) {
- return Collections.emptyMap();
- }
- Map<String, String> map = new LinkedHashMap<String, String>();
- String[] array = persistenceInfo.split("\r\n");
- for (String line : array) {
- String[] cells = line.split(":");
- if (cells.length > 1) {
- map.put(cells[0], cells[1]);
- }
- }
- return map;
- }
- public boolean invokeBgRewriteAof(final Jedis jedis) {
- return new IdempotentConfirmer() {
- @Override
- public boolean execute() {
- try {
- String response = jedis.bgrewriteaof();
- if (response != null && response.contains("rewriting started")) {
- return true;
- }
- } catch (Exception e) {
- String message = e.getMessage();
- if (message.contains("rewriting already")) {
- return true;