作者:谢泽华

背景

众所周知单个机房在出现不可抗拒的问题(如断电、断网等因素)时,会导致无法正常提供服务,会对业务造成潜在的损失。所以在协同办公领域,一种可以基于同城或异地多活机制的高可用设计,在保障数据一致性的同时,能够最大程度降低由于机房的仅单点可用所导致的潜在高可用问题,最大程度上保障业务的用户体验,降低单点问题对业务造成的潜在损失显得尤为重要。

同城双活,对于生产的高可用保障,重大的意义和价值是不可言喻的。表面上同城双活只是简单的部署了一套生产环境而已,但是在架构上,这个改变的影响是巨大的,无状态应用的高可用管理、请求流量的管理、版本发布的管理、网络架构的管理等,其提升的架构复杂度巨大。

结合真实的协同办公产品:京办(为北京市政府提供协同办公服务的综合性平台)生产环境面对的复杂的政务网络以及京办同城双活架构演进的案例,给大家介绍下京办持续改进、分阶段演进过程中的一些思考和实践经验的总结。本文仅针对ES集群在跨机房同步过程中的方案和经验进行介绍和总结。

架构

1.部署Logstash在金山云机房上,Logstash启动多个实例(按不同的类型分类,提高同步效率),并且和金山云机房的ES集群在相同的VPC

2.Logstash需要配置大网访问权限,保证Logstash和ES原集群和目标集群互通。

3.数据迁移可以全量迁移和增量迁移,首次迁移都是全量迁移后续的增加数据选择增量迁移。

4.增量迁移需要改造增加识别的增量数据的标识,具体方法后续进行介绍。

原理

Logstash工作原理

Logstash分为三个部分input 、filter、ouput:

1.input处理接收数据,数据可以来源ES,日志文件,kafka等通道.

2.filter对数据进行过滤,清洗。

3.ouput输出数据到目标设备,可以输出到ES,kafka,文件等。

增量同步原理

  1. 对于T时刻的数据,先使用Logstash将T以前的所有数据迁移到有孚机房京东云ES,假设用时∆T

  2. 对于T到T+∆T的增量数据,再次使用logstash将数据导入到有孚机房京东云的ES集群

  3. 重复上述步骤2,直到∆T足够小,此时将业务切换到华为云,最后完成新增数据的迁移

适用范围:ES的数据中带有时间戳或者其他能够区分新旧数据的标签

流程

准备工作

1.创建ECS和安装JDK忽略,自行安装即可

2.下载对应版本的Logstash,尽量选择与Elasticsearch版本一致,或接近的版本安装即可

https://www.elastic.co/cn/downloads/logstash

1) 源码下载直接解压安装包,开箱即用

2)修改对内存使用,logstash默认的堆内存是1G,根据ECS集群选择合适的内存,可以加快集群数据的迁移效率。

  1. 迁移索引

Logstash会帮助用户自动创建索引,但是自动创建的索引和用户本身的索引会有些许差异,导致最终数据的搜索格式不一致,一般索引需要手动创建,保证索引的数据完全一致。

以下提供创建索引的python脚本,用户可以使用该脚本创建需要的索引。

create_mapping.py文件是同步索引的python脚本,config.yaml是集群地址配置文件。

注:使用该脚本需要安装相关依赖

yum install -y PyYAML
yum install -y python-requests

拷贝以下代码保存为 create_mapping.py:

import yaml
import requests
import json
import getopt
import sys def help():
print
"""
usage:
-h/--help print this help.
-c/--config config file path, default is config.yaml example:
python create_mapping.py -c config.yaml
"""
def process_mapping(index_mapping, dest_index):
print(index_mapping)
# remove unnecessary keys
del index_mapping["settings"]["index"]["provided_name"]
del index_mapping["settings"]["index"]["uuid"]
del index_mapping["settings"]["index"]["creation_date"]
del index_mapping["settings"]["index"]["version"] # check alias
aliases = index_mapping["aliases"]
for alias in list(aliases.keys()):
if alias == dest_index:
print(
"source index " + dest_index + " alias " + alias + " is the same as dest_index name, will remove this alias.")
del index_mapping["aliases"][alias]
if index_mapping["settings"]["index"].has_key("lifecycle"):
lifecycle = index_mapping["settings"]["index"]["lifecycle"]
opendistro = {"opendistro": {"index_state_management":
{"policy_id": lifecycle["name"],
"rollover_alias": lifecycle["rollover_alias"]}}}
index_mapping["settings"].update(opendistro)
# index_mapping["settings"]["opendistro"]["index_state_management"]["rollover_alias"] = lifecycle["rollover_alias"]
del index_mapping["settings"]["index"]["lifecycle"]
print(index_mapping)
return index_mapping
def put_mapping_to_target(url, mapping, source_index, dest_auth=None):
headers = {'Content-Type': 'application/json'}
create_resp = requests.put(url, headers=headers, data=json.dumps(mapping), auth=dest_auth)
if create_resp.status_code != 200:
print(
"create index " + url + " failed with response: " + str(create_resp) + ", source index is " + source_index)
print(create_resp.text)
with open(source_index + ".json", "w") as f:
json.dump(mapping, f)
def main():
config_yaml = "config.yaml"
opts, args = getopt.getopt(sys.argv[1:], '-h-c:', ['help', 'config='])
for opt_name, opt_value in opts:
if opt_name in ('-h', '--help'):
help()
exit()
if opt_name in ('-c', '--config'):
config_yaml = opt_value config_file = open(config_yaml)
config = yaml.load(config_file)
source = config["source"]
source_user = config["source_user"]
source_passwd = config["source_passwd"]
source_auth = None
if source_user != "":
source_auth = (source_user, source_passwd)
dest = config["destination"]
dest_user = config["destination_user"]
dest_passwd = config["destination_passwd"]
dest_auth = None
if dest_user != "":
dest_auth = (dest_user, dest_passwd)
print(source_auth)
print(dest_auth) # only deal with mapping list
if config["only_mapping"]:
for source_index, dest_index in config["mapping"].iteritems():
print("start to process source index" + source_index + ", target index: " + dest_index)
source_url = source + "/" + source_index
response = requests.get(source_url, auth=source_auth)
if response.status_code != 200:
print("*** get ElasticSearch message failed. resp statusCode:" + str(
response.status_code) + " response is " + response.text)
continue
mapping = response.json()
index_mapping = process_mapping(mapping[source_index], dest_index) dest_url = dest + "/" + dest_index
put_mapping_to_target(dest_url, index_mapping, source_index, dest_auth)
print("process source index " + source_index + " to target index " + dest_index + " successed.")
else:
# get all indices
response = requests.get(source + "/_alias", auth=source_auth)
if response.status_code != 200:
print("*** get all index failed. resp statusCode:" + str(
response.status_code) + " response is " + response.text)
exit()
all_index = response.json()
for index in list(all_index.keys()):
if "." in index:
continue
print("start to process source index" + index)
source_url = source + "/" + index
index_response = requests.get(source_url, auth=source_auth)
if index_response.status_code != 200:
print("*** get ElasticSearch message failed. resp statusCode:" + str(
index_response.status_code) + " response is " + index_response.text)
continue
mapping = index_response.json() dest_index = index
if index in config["mapping"].keys():
dest_index = config["mapping"][index]
index_mapping = process_mapping(mapping[index], dest_index) dest_url = dest + "/" + dest_index
put_mapping_to_target(dest_url, index_mapping, index, dest_auth)
print("process source index " + index + " to target index " + dest_index + " successed.") if __name__ == '__main__':
main()

配置文件保存为config.yaml:

# 源端ES集群地址,加上http://
source: http://ip:port
source_user: "username"
source_passwd: "password"
# 目的端ES集群地址,加上http://
destination: http://ip:port
destination_user: "username"
destination_passwd: "password" # 是否只处理这个文件中mapping地址的索引
# 如果设置成true,则只会将下面的mapping中的索引获取到并在目的端创建
# 如果设置成false,则会取源端集群的所有索引,除去(.kibana)
# 并且将索引名称与下面的mapping匹配,如果匹配到使用mapping的value作为目的端的索引名称
# 如果匹配不到,则使用源端原始的索引名称
only_mapping: true # 要迁移的索引,key为源端的索引名字,value为目的端的索引名字
mapping:
source_index: dest_index

以上代码和配置文件准备完成,直接执行 python create_mapping.py 即可完成索引同步。

索引同步完成可以取目标集群的kibana上查看或者执行curl查看索引迁移情况:

GET _cat/indices?v

全量迁移

Logstash配置位于config目录下。

用户可以参考配置修改Logstash配置文件,为了保证迁移数据的准确性,一般建议建立多组Logstash,分批次迁移数据,每个Logstash迁移部分数据。

配置集群间迁移配置参考:

input{
elasticsearch{
# 源端地址
hosts => ["ip1:port1","ip2:port2"]
# 安全集群配置登录用户名密码
user => "username"
password => "password"
# 需要迁移的索引列表,以逗号分隔,支持通配符
index => "a_*,b_*"
# 以下三项保持默认即可,包含线程数和迁移数据大小和logstash jvm配置相关
docinfo=>true
slices => 10
size => 2000
scroll => "60m"
}
} filter {
# 去掉一些logstash自己加的字段
mutate {
remove_field => ["@timestamp", "@version"]
}
} output{
elasticsearch{
# 目的端es地址
hosts => ["http://ip:port"]
# 安全集群配置登录用户名密码
user => "username"
password => "password"
# 目的端索引名称,以下配置为和源端保持一致
index => "%{[@metadata][_index]}"
# 目的端索引type,以下配置为和源端保持一致
document_type => "%{[@metadata][_type]}"
# 目标端数据的_id,如果不需要保留原_id,可以删除以下这行,删除后性能会更好
document_id => "%{[@metadata][_id]}"
ilm_enabled => false
manage_template => false
} # 调试信息,正式迁移去掉
stdout { codec => rubydebug { metadata => true }}
}

增量迁移

预处理:

  1. @timestamp 在elasticsearch2.0.0beta版本后弃用

https://www.elastic.co/guide/en/elasticsearch/reference/2.4/mapping-timestamp-field.html

  1. 本次对于京办从金山云机房迁移到京东有孚机房,所涉及到的业务领域多,各个业务线中所代表新增记录的时间戳字段不统一,所涉及到的兼容工作量大,于是考虑通过elasticsearch中预处理功能pipeline进行预处理添加统一增量标记字段:gmt_created_at,以减少迁移工作的复杂度(各自业务线可自行评估是否需要此步骤)。
PUT _ingest/pipeline/gmt_created_at
{
"description": "Adds gmt_created_at timestamp to documents",
"processors": [
{
"set": {
"field": "_source.gmt_created_at",
"value": "{{_ingest.timestamp}}"
}
}
]
}
  1. 检查pipeline是否生效
GET _ingest/pipeline/*
  1. 各个index设置对应settings增加pipeline为默认预处理
PUT index_xxxx/_settings
{
"settings": {
"index.default_pipeline": "gmt_created_at"
}
}
  1. 检查新增settings是否生效
GET index_xxxx/_settings

增量迁移脚本

schedule-migrate.conf

index:可以使用通配符的方式

query: 增量同步的DSL,统一gmt_create_at为增量同步的特殊标记

schedule: 每分钟同步一把,"* * * * *"

input {
elasticsearch {
hosts => ["ip:port"]
# 安全集群配置登录用户名密码
user => "username"
password => "password"
index => "index_*"
query => '{"query":{"range":{"gmt_create_at":{"gte":"now-1m","lte":"now/m"}}}}'
size => 5000
scroll => "5m"
docinfo => true
schedule => "* * * * *"
}
}
filter {
mutate {
remove_field => ["source", "@version"]
}
}
output {
elasticsearch {
# 目的端es地址
hosts => ["http://ip:port"]
# 安全集群配置登录用户名密码
user => "username"
password => "password"
index => "%{[@metadata][_index]}"
document_type => "%{[@metadata][_type]}"
document_id => "%{[@metadata][_id]}"
ilm_enabled => false
manage_template => false
} # 调试信息,正式迁移去掉
stdout { codec => rubydebug { metadata => true }}
}

问题:

mapping中存在join父子类型的字段,直接迁移报400异常

[2022-09-20T20:02:16,404][WARN ][logstash.outputs.elasticsearch] Could not index event to Elasticsearch. {:status=>400,
:action=>["index", {:_id=>"xxx", :_index=>"xxx", :_type=>"joywork_t_work", :routing=>nil}, #<LogStash::Event:0x3b3df773>],
:response=>{"index"=>{"_index"=>"xxx", "_type"=>"xxx", "_id"=>"xxx", "status"=>400,
"error"=>{"type"=>"mapper_parsing_exception", "reason"=>"failed to parse",
"caused_by"=>{"type"=>"illegal_argument_exception", "reason"=>"[routing] is missing for join field [task_user]"}}}}}

解决方法:

https://discuss.elastic.co/t/an-routing-missing-exception-is-obtained-when-reindex-sets-the-routing-value/155140 https://github.com/elastic/elasticsearch/issues/26183

结合业务特征,通过在filter中加入小量的ruby代码,将_routing的值取出来,放回logstah event中,由此问题得以解决。

示例:

跨机房ES同步实战的更多相关文章

  1. 禧云Redis跨机房双向同步实践

    编者荐语: 2019年4月16日跨机房Redis同步中间件(Rotter)上线,团餐率先商用: 以下文章来源于云纵达摩院 ,作者杨海波   禧云信息/研发中心/杨海波 20191115 关键词:Rot ...

  2. Linux实战教学笔记48:openvpn架构实施方案(一)跨机房异地灾备

    第一章VPN介绍 1.1 VPN概述 VPN(全称Virtual Private Network)虚拟专用网络,是依靠ISP和其他的NSP,在公共网络中建立专用的数据通信网络的技术,可以为企业之间或者 ...

  3. 阿里Canal框架数据库同步-实战教程

    一.Canal简介: canal是阿里巴巴旗下的一款开源项目,纯Java开发.基于数据库增量日志解析,提供增量数据订阅&消费,目前主要支持了MySQL(也支持mariaDB). 二.背景介绍: ...

  4. 最近帮客户实施的基于SQL Server AlwaysOn跨机房切换项目

    最近帮客户实施的基于SQL Server AlwaysOn跨机房切换项目 最近一个来自重庆的客户找到走起君,客户的业务是做移动互联网支付,是微信支付收单渠道合作伙伴,数据库里存储的是支付流水和交易流水 ...

  5. SQL Server 跨网段(跨机房)FTP复制

    一.本文所涉及的内容(Contents) 本文所涉及的内容(Contents) 背景(Contexts) 搭建过程(Process) 注意事项(Attention) 参考文献(References) ...

  6. SQL Server 跨网段(跨机房)复制

    一.本文所涉及的内容(Contents) 本文所涉及的内容(Contents) 背景(Contexts) 解决方案(Solution) 搭建过程(Process) 注意事项(Attention) 参考 ...

  7. SQL Server跨网段(跨机房)FTP复制

    SQL Server跨网段(跨机房)FTP复制 2013-09-24 17:53 by 听风吹雨, 273 阅读, 0 评论, 收藏, 编辑 一. 背景 搭建SQL Server复制的时候,如果网络环 ...

  8. Step5:SQL Server 跨网段(跨机房)FTP复制

    一.本文所涉及的内容(Contents) 本文所涉及的内容(Contents) 背景(Contexts) 搭建过程(Process) 注意事项(Attention) 参考文献(References) ...

  9. vitess元数据跨机房灾备解决方案

    测试使用vitess的时候发现vitess元数据的实现有多种方案,etcd, etcd2, zk,zk2, 由于刚开始测试的时候使用的是基于k8s集群+etcd的,以下就分步说明灾备实现方案: 1. ...

  10. etcd跨机房部署方案

    使用ETCD做为元数据方便快捷,但是谈到跨机房灾备可能就迷糊了,我们在做节日灾备的时候同样遇到了问题, 通过查阅官方文档找到了解决方案,官方提供make-mirror方法,提供数据镜像服务 注意: m ...

随机推荐

  1. Solutions:安全的APM服务器访问

    转载自: https://blog.csdn.net/UbuntuTouch/article/details/105527468 APM Agents 访问APM server如果不做安全的设置,那么 ...

  2. Elasticsearch 开发入门 - Python

    文章转载自:https://elasticstack.blog.csdn.net/article/details/111573923 前提条件 你需要在你的电脑上安装 python3 你需要安装 do ...

  3. switch分支

    说明: 当表达式的值等于case中的常量,则会执行其中包含的语句块 break用于跳出循环,如果不写,则直接执行下一个常量的语句块,不再去判断表达式的值是否等于下一个case的常量(case穿透) 最 ...

  4. typora基础和计算机五大组成部分

    typora typora软件 ​ 是一款适合于IT行业文本编辑器,笔记,当下来说,非常火爆,可以使用多种语言,python java... ​ 安装的时候路径选择可以设置一些简单便于后续查找的文件路 ...

  5. C#-12 转换

    一 什么是转换 转换是接受一个类型的值并使用它作为另一个类型的等价值的过程. 下列代码演示了将1个short类型的值强制转换成byte类型的值. short var1 = 5; byte var2 = ...

  6. scss的使用方法

    引用scss文件--看上一篇的less使用,里面的Koala,一样的原理!!! 方法一: scss: /**定义变量 */$width:404px;$color:green;$font-size:20 ...

  7. 学习记录-Python的局部变量和全局变量

    目录 1 定义 2 作用域的重要性 2.1 全局作用域中的代码不能使用任何局部变量 2.2 局部作用域中的代码可以访问全局变量 2.3 不同局部作用域中的变量不能相互调用 2.4 在不同的作用域中,可 ...

  8. 洛谷P1036 [NOIP2002 普及组] 选数 (搜索)

    n个数中选取k个数,判断这k个数的和是否为质数. 在dfs函数中的状态有:选了几个数,选的数的和,上一个选的数的位置: 试除法判断素数即可: 1 #include<bits/stdc++.h&g ...

  9. day45-JDBC和连接池01

    JDBC和连接池01 1.JDBC概述 基本介绍 JDBC为访问不同的数据库提供了同一的接口,为使用者屏蔽了细节问题 Java程序员使用JDBC,可以连接任何提供了jdbc驱动程序的数据库系统,从而完 ...

  10. Condition介绍

    Condition Condition是一种多线程通信工具,表示多线程下参与数据竞争的线程的一种状态,主要负责多线程环境下对线程的挂起和唤醒工作. 方法 // ========== 阻塞 ====== ...