能力说明:
掌握封装、继承和多态设计Java类的方法,能够设计较复杂的Java类结构;能够使用泛型与集合的概念与方法,创建泛型类,使用ArrayList,TreeSet,TreeMap等对象掌握Java I/O原理从控制台读取和写入数据,能够使用BufferedReader,BufferedWriter文件创建输出、输入对象。
阿里云技能认证
详细说明
为了比较方便统计微服务服务器基础指标和相关运行状态,目前比较流行和成熟的方案是采用Prometheus+Grafana。本文主要基于docker的方式,快速搭建一套监控体系。 应用改造 为了整合Prometheus,需要在微服务应用中增加micrometer-registry-prometheus依赖: <!-- Micrometer Prometheus registry --> <dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-registry-prometheus</artifactId> </dependency> 配置中打开端点/actuator/prometheus management: endpoints: web: exposure: include: "*" endpoint: health: show-details: ALWAYS metrics: tags: application: ${spring.application.name} 启动prometheus 运行prometheus容器: $ docker run -d --name=prometheus -p 9090:9090 -v /docker/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml prom/prometheus:v2.16.0 --config.file=/etc/prometheus/prometheus.yml 其中映射到主机的配置文件prometheus.yml的配置如下: # my global config global: scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute. evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute. # scrape_timeout is set to the global default (10s). # Load rules once and periodically evaluate them according to the global 'evaluation_interval'. # rule_files: # - "first_rules.yml" # - "second_rules.yml" # A scrape configuration containing exactly one endpoint to scrape: # Here it's Prometheus itself. scrape_configs: # The job name is added as a label `job=<job_name>` to any timeseries scraped from this config. - job_name: 'prometheus' # metrics_path defaults to '/metrics' # scheme defaults to 'http'. static_configs: - targets: ['127.0.0.1:9090'] - job_name: 'nacos-prometheus' metrics_path: '/actuator/prometheus' scrape_interval: 15s consul_sd_configs: - server: '192.168.0.102:7000' services: [] 启动Grafana 使用以下命令可以使Docker下载和运行Grafana: $ docker run -d --name=grafana -p 3000:3000 grafana/grafana 为了能让Grafana的面板嵌入到应用中,需要更改配置以root用户进入容器中 $ docker exec -u root -it grafana sh # vi conf/defaults.ini 导入模版 使用dashboard模版,可以减少定制的时间。官网的dashboard广场,有很多现成的:https://grafana.com/grafana/dashboards在Grafana的面板中点击菜单Import: 从官网dashboard广场获取面板ID: 此处粘贴入模版ID: 微服务应用,推荐使用4701 将Grafana嵌入自己的应用页面 dashboard 和 panel都可以分享,如图点击share:弹出的对话框中,可以自定义一些参数 分享出来的链接加入kiosk=tv参数还可以隐藏面板的左边菜单和上面标题栏。然后就可以用iframe标签嵌入到前端页面里去了。 告警配置 如需告警,则需要把配置文件中的这个地址改下,否则docker里的会是错误的在Grafana的Alert面板中,可以根据需要配置告警的一些参数,如监测频率等 生成图文告警 首先在Grafana的Notification channels面板中配置告警通道:有2个参数必须注意,Url填入告警分发的钉钉机器人的webhook。如何添加群机器人并获得webkook,请参考钉钉官网https://ding-doc.dingtalk.com/doc#/serverapi2/krgddi。此外,要注意勾选 Include image 选项。配置好后,Grafana的告警图表还需要一个外部的图像渲染服务。我们依然使用docker来创建并启动此服务,新建容器并对外开放8081端口: $ docker run -d --name=grafana-image-renderer -p 8081:8081 --env GF_RENDERER_PLUGIN_TZ=Asia/Shanghai --env GF_RENDERER_PLUGIN_IGNORE_HTTPS_ERRORS=true grafana/grafana-image-renderer 进入容器中修改配置文件 /conf/defaults.ini [rendering] # 这里指定使用外部图像渲染,指向刚建立的容器 /render 路径不可少 server_url = http://192.168.0.102:8081/render # If the remote HTTP image renderer service runs on a different server than the callback_url = http://192.168.0.102:3000 还有: [external_image_storage] # 使用s3协议 provider = s3 [external_image_storage.s3] # 这个链接必须要能连通互联网,否则图像能生成,但会无法访问 endpoint = http://www.xxx.com # 这个参数必须设置成true,否则下面的bucket参数会加在上面endpoint最前面,导致访问不了,会变成 http://mybucket.www.xxx.com path_style_access = true # 不要配置 bucket_url = bucket = mybucket # minIO默认的region是us-east-1 region = us-east-1 path = images access_key = AKIAIOSFODNN7EXAMPLE secret_key = wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY中的 我们用的是本地的minIO作为图像生成的存储服务。当然用阿里云的OSS应该也是没有问题的。 通过以上的配置,当Grafana监测到系统指标超过告警阀值,则就会向钉钉发送图文消息
本文通过钉钉机器人调用函数计算实现的serverless web服务,打通物联网平台,和树莓派实时通讯。实现了将原有传统的磁吸门禁,改造成可以由钉钉来控制开门的简单应用。 场景 由于本部门拥有独立封闭的空间,在大门口配置了磁吸玻璃门,因此规定在工作期间出入需要随手关门,以保证工作环境的私密性和安全性。但前台并没有小妹,这样对于来访客人就不是特别方便,往往需要电话通知接待者到大门口来接,要么接待者向管理员要来无线遥控器来开门(tuo ku zi fang pi)。这对于我这种重度懒惰病患者来说,简直是种折磨。So... 思路 是否可以由树莓派模拟按动无线门禁遥控器的按钮来开门?经过对无线遥控器的暴力拆解,发现遥控器的开门按钮是由一个有4个引脚的贴片按钮构成。经过试验,只需把这2个接口短路,就相当于按下按钮的动作。因此可以在上面接驳一个继电器,由树莓派控制继电器来实现遥控器的按钮接通。图参考3.1节 目标: 将有经常需要接待的人(重度懒惰病患者)加入到公司某个内部钉钉群中,有来访者需要开门时,直接向群机器人发送"开门",门禁就会打开。 实现方式: 在钉钉群加入一个自定义的outgoing机器人,向机器人发送指令后,远程HTTP POST调用函数计算服务,函数计算向物联网平台的Topic上送消息,而订阅此Topic的树莓派会在收到消息后打开继电器开关,让无线遥控器的按钮短路,发送无线信号让门禁开启。 成本估算 物联网平台:使用基础版产品免费函数计算:调用次数:每月前 100 万次函数调用免费。执行时间:每月前 400000(GB*秒) 费用免费。唯一可能产生费用的就是极少量的网络使用费 准备 物料准备 树莓派 一路继电器 门禁无线遥控器 母对母杜邦线3根 公对公杜邦线2根 阿里云环境准备 物联网平台 函数计算 日志服务(可选) 操作步骤 1 云端开发 1.1 物联网平台 登录阿里云控制台,进入物联网平台控制面板 1.1.1 新建产品 进入设备管理,创建产品,选择基础版或高级版都可以,本实例使用基础版就可以满足基本要求。系统会自动创建3个Topic,我们需要使用 /ProductName/${deviceName}/get,作为设备订阅消息的Topic。 1.1.2 设备管理 在产品中新增设备,并获得设备的3元组,在2.3节的设备代码的编写时需要使用此3元组。设备三元组是设备的唯一标示 1.2 函数计算 函数计算可以通过flask web工程来实现serverless,我们可以直接使用url去访问和调用函数。好处在于,直接在函数计算里编写代码即可,而省去了购买ECS及搭建相应的运行环境,使用起来非常方便。通过使用http触发器,函数计算可提供 http://${account-id}.${region}.fc.aliyuncs.com/2016-08-15/proxy/$(serviceName)/$(functionName)/ 来访问调用函数。 1.2.1 新建服务 新建服务 iot-demo,如果需要记录和回溯函数执行的日志,则需要开通日志服务,并配置好日志仓库。 1.2.2 新建函数 新建函数,使用模版 flask-web,函数名和触发器填写 officegate,运行环境选择python2.7 1.2.3 函数代码 # -*- coding: utf-8 -*- from flask import Flask from flask import request from flask import make_response import logging from aliyunsdkcore import client from aliyunsdkcore.request import RpcRequest import base64 try: from urllib.parse import urlparse except: from urlparse import urlparse app = Flask(__name__) base_path = '' return_string='''{ "msgtype": "text", "text": { "content": "%s" } }''' # 参数定义 options = { 'productKey': '', # 设备标识三元组 'deviceName': '', # 设备标识三元组 'accessKeyId': '', 'accessKeySecret': '', 'token': '12345678', # Dingding Outgoing token } clt = client.AcsClient(options['accessKeyId'], options['accessKeySecret'], 'cn-shanghai') # 推送消息到IoT Hub Topic def pushMsg(msg): request = RpcRequest('Iot', '2018-01-20', 'Pub') request.set_accept_format('json') request.add_query_param('ProductKey',options['productKey']) request.add_query_param('TopicFullName','/' + options['productKey'] + '/' + options['deviceName'] +'/get') #消息发送到的Topic全名 request.add_query_param('MessageContent',base64.b64encode(msg)) #Base64 request.add_query_param('Qos',0) result = clt.do_action_with_exception(request) logging.info('result : ' + result) # 健康检查 @app.route('/', methods=['GET', 'POST']) def home(): resp = make_response('I am ok!', 200) return resp # 对应 DingDing outgoing URL @app.route('/dingbot',methods=['POST']) def bot_receive() : token=request.headers.get('token') data=request.get_json() logging.debug("token="+token+"\nmessage:"+str(data)) msgtype=data.get("msgtype") if token != options['token'] and msgtype != "text" : return make_response("error",403) content=str(data["text"]["content"]) senderId=data["senderId"] senderNick=data["senderNick"] logging.info('%s(%s) talk: %s' % (senderNick,senderId,content)) pushMsg(content.strip()) ret = return_string % "门开了" resp = make_response(ret,200) resp.headers['Content-Type']="application/json; charset=utf-8" return resp # 函数计算主入口 def handler(environ, start_response): parsed_tuple = urlparse(environ['fc.request_uri']) li = parsed_tuple.path.split('/') global base_path if not base_path: base_path = "/".join(li[0:5]) return app(environ, start_response) 代码创建完成后,在代码编辑框下方的调试Http触发器,可以获得调用的url。注意,我们代码里路由入口是/dingbot因此,实际的访问网址应该是:https://$(account-id).cn-hangzhou.fc.aliyuncs.com/2016-08-15/proxy/iot-demo/officegate/dingbot 2 钉钉机器人的配置 如图所示,添加自定义机器人时,需要勾选中 是否开启Outgoing机制,在POST地址里,把上一节中的url粘贴进来,而Token必须和函数计算代码里参数配置的一致,本例中是12345678。添加成功后,尝试着给钉钉机器人发指令“开门”,如果机器人能回应“门开了”,那说明函数计算已经可以正常提供服务。 3 设备端开发 3.1 硬件安装 首先把门禁遥控器的外壳拆开(大卸八块) 用电烙铁把开门按钮的4个引脚从电路板上取下来 把2根公杜邦线的引脚焊接在电路板的2个焊点上,并把另一头分别接在继电器A、B端上 再拿3根母杜邦线,VCC、GND、IN分别接到树莓派GPIO接口的4、6、16引脚上。 3.2 环境准备 我们在树莓派上使用python2.7作为开发语言。安装阿里云物联网的SDK: pip install aliyun-python-sdk-iot-client 3.3 代码开发 gate-demo.py 内容如下: # -*- coding: utf-8 -*- import json from time import sleep import RPi.GPIO as GPIO import aliyunsdkiotclient.AliyunIotMqttClient as iot # 参数配置 options = { 'productKey':'', # 设备三元组 'deviceName':'', # 设备三元组 'deviceSecret':'', # 设备三元组 'port':1883, 'host':'iot-as-mqtt.cn-shanghai.aliyuncs.com', 'gate_pin':23 #in 接GPIO 23针脚 } # 订阅的IoT Hub Topic SUBSCRIBE_TOPIC = '/'+options['productKey']+'/'+options['deviceName']+'/user/get' GPIO.setmode(GPIO.BCM) GPIO.setwarnings(False) # 设置GPIO接口是输出,默认低电平 GPIO.setup(options['gate_pin'],GPIO.OUT,initial=GPIO.LOW) # 开门 def openGate() : GPIO.output(options['gate_pin'],GPIO.HIGH) # 打开高电平 sleep(0.8) GPIO.output(options['gate_pin'],GPIO.LOW) # 恢复低电平 # 收到发布的消息回调 def on_message(client, userdata, msg): print(msg.payload) if msg.payload=='开门': openGate() def on_connect(client, userdata, flags_dict, rc): print("Connected with result code " + str(rc)) # 订阅消息 client.subscribe(topic=SUBSCRIBE_TOPIC) def on_disconnect(client, userdata, flags_dict, rc): print("Disconnected.") if __name__ == '__main__': # IoT 初始化 client = iot.getAliyunIotMqttClient(options['productKey'], options['deviceName'], options['deviceSecret'], secure_mode=3) client.on_connect = on_connect client.on_message = on_message client.connect(host=options['productKey'] + '.' + options['host'], port=options['port'], keepalive=60) client.loop_forever() 4 测试运行 4.1 设备端运行 在py-demo文件夹下运行 python gate-demo.py 4.2 钉钉发送消息 在钉钉群上 @机器人,发送“开门”指令,如果函数计算运行正常,则应能收到机器人“门开了”的应答。 4.3 云端查看上送消息 在函数计算里的日志查询界面中,可以看到程序运行时记录的日志:在物联网平台的设备的Topic列表中,可以看到收到了函数计算发布的消息: 4.4 测试结果 树莓派的python程序打印出日志,继电器的信号灯闪动,同时遥控器发出无线指令,大门磁吸门禁收到信号后开启。 后续 本例只是一个基于物联网的简单用例,并未考虑过多的安全问题。下一步可以对安全性进一步完善,例如,授权和控制谁能够进行开门等。此外,提高系统的安全性,还引入API网关,便于进行访问、流量控制等。 总结 阿里云提供的一系列产品和服务,就像拧开水龙头一样,打开即用。通过物联网平台,使用者可以快速地将原有传统硬件产品迅速地进行升级改造,实现设备迅速上云。
本文通过一个简单实例,主要介绍了如何使用树莓派快速接入阿里云iot platform,并实现了一个简易的监控人员出入并拍照上送钉钉群的场景 场景 在公司大门入口处布点树莓派和红外感应,实现出入口人员出入时,自动拍照并上送钉钉群机器人 准备 物料准备 树莓派 HC-SR501 人体红外感应器 树莓派摄像头 母对母杜邦线三根 阿里云环境准备 物联网平台 对象存储OSS 函数计算 日志服务(可选) 操作步骤 1 云端开发 1.1 物联网平台 登录阿里云控制台,进入物联网平台控制面板 1.1.1 新建产品 进入设备管理,创建产品,选择基础版或高级版都可以,本实例使用基础版就可以满足基本要求。系统会自动创建3个Topic,我们需要使用 /ProductName/${deviceName}/update,作为设备告警消息的上送的Topic。 1.1.2 设备管理 在产品中新增设备,并获得设备的3元组,在2.3节的设备代码的编写时需要使用此3元组。设备三元组是设备的唯一标示 1.1.3 新建规则引擎 设置规则引擎的意义在于,可以将设备上送的消息数据,通过配置转发规则将处理后的数据转发到阿里云其他服务,例如RDS、TBS和函数计算等等。我们需要注意的是从设备端到规则引擎处理后的JSON数据格式的变化,下图中是基础版的演变过程:我们在设备端消息上送定义的JSON的格式是: { 'photo': 'xxxxxxx.jpg' } 新创建一个规则,数据格式选择JSON。编写处理数据的SQL SELECT deviceName() deviceName, photo FROM "/a1O4b4XcICc/+/update" 配置完成后,我们可以模拟调试一下,检查规则是否正确:接着,配置数据转发,把数据转发到FC函数计算中。分别选择在1.3步骤中创建好的服务和函数。 1.2 对象存储 由于设备端拍摄的照片需要在钉钉中展示,因此把照片存储在OSS上是一个解决方案。 1.2.1 新建bucket 新建一个bucket用于存储设备上送的照片。读写权限选择公共读然后在bucket中创建photo目录。 1.3 函数计算 经过物联网平台规则引擎转发过来的JSON数据,我们通过建立函数,把它转发到钉钉机器人接口上,实现钉钉群中的消息通知 1.3.1 新建服务 新创建服务,如果需要记录和回溯函数执行的日志,则需要开通日志服务,配置日志仓库。 1.3.2 新建函数 使用空白模版新建函数,不需要触发器,运行环境选择python2.7 1.3.3 函数代码 # -*- coding: utf-8 -*- import logging import json import requests # 钉钉消息发送实现 def post(data): webhook_url='https://oapi.dingtalk.com/robot/send?access_token=${Token}' #钉钉群机器人的webhook的URL headers = {'Content-Type': 'application/json; charset=utf-8'} post_data = json.dumps(data) try: response = requests.post(webhook_url, headers=headers, data=post_data) logging.info('Send success') except requests.exceptions.HTTPError as exc: logging.error("Send Error,HTTP error: %d, reason: %s" % (exc.response.status_code, exc.response.reason)) raise except requests.exceptions.ConnectionError: logging.error("Send Error,HTTP connection error!") raise else: result = response.json() logging.info('Send Error:%s' % result) if result['errcode']: error_data = {"msgtype": "text", "text": {"content": "Send Error, reason:%s" % result['errmsg']}, "at": {"isAtAll": True}} logging.error("Send Error:%s" % error_data) requests.post(webhook_url, headers=headers, data=json.dumps(error_data)) return result # 发送钉钉markdown消息 def post_markdown(title,text): data = { "msgtype": "markdown", "markdown": { "title": title, "text": text }, "at": { "atMobiles": [], "isAtAll": False } } post(data) # 函数计算入口 def handler(event, context): logger = logging.getLogger() evt = json.loads(event) #OSS endpoint url post_markdown('告警','' % evt.get('photo','')) logger.info('photo name is %s', evt.get('photo','')) return 'OK' 2 设备端开发 HC-SR501模块感应到有人移动时,会输出高电平,则触发摄像头拍照,并将照片文件存储到OSS,同时发送消息到IOT平台的/ProductName/${deviceName}/update消息队列中 2.1 硬件安装 连接好摄像头 将HC-SR501 人体红外感应器的vcc引脚接5v,也就是pin4,I/O引脚接pin18,GND引脚接地pin6 2.2 环境准备 我们在树莓派上使用python2.7作为开发语言。 1. pip install aliyun-python-sdk-iot-client 2. pip install oss2 3. mkdir py-demo (项目程序文件夹) 4. cd py-demo 5. mkdir photo (本地照片临时目录) 6. vim monitor.py 2.3 代码开发 monitor.py 内容如下: # -*- coding: utf-8 -*- import json import uuid import logging from time import sleep from picamera import PiCamera import RPi.GPIO as GPIO import oss2 import aliyunsdkiotclient.AliyunIotMqttClient as iot # 参数定义 options = { 'productKey': '${productKey}', # 设备标识三元组 'deviceName': '${deviceName}', # 设备标识三元组 'deviceSecret': '${deviceSecret}', # 设备标识三元组 'port': 1883, # iot mqtt port 'host': 'iot-as-mqtt.cn-shanghai.aliyuncs.com', # iot mqtt endpoint 'endpoint': 'http://oss-cn-hangzhou.aliyuncs.com', # oss endpoint 'ak': '${ak}', 'sk': '${sk}', 'bucket': '${bucket}', # oss bucket 'ir_pin': 24 # 人体红外感应器设置读取针脚标号 } topic = '/' + options['productKey'] + '/' + options['deviceName'] + '/user/test' # 拍照存oss,并发送通知 def takephoto2oss(client): #拍照 photo_filename = str(uuid.uuid1()) + ".jpg" print('take photo :' + photo_filename) camera.capture('photo/' + photo_filename) #存OSS print('save photo to oss :' + photo_filename) bucket.put_object_from_file( 'photo/' + photo_filename, 'photo/' + photo_filename) #消息上送 payload_json = { 'photo': photo_filename } print('send data to iot server: ' + str(payload_json)) client.publish(topic, payload = str(payload_json)) def on_connect(client, userdata, flags_dict, rc): print("Connected with result code " + str(rc)) def on_disconnect(client, userdata, flags_dict, rc): print("Disconnected.") if __name__ == '__main__': # GPIO 初始化 GPIO.setmode(GPIO.BCM) GPIO.setwarnings(False) GPIO.setup(options['ir_pin'], GPIO.IN) # 摄像头 初始化 camera = PiCamera() camera.resolution = (640, 480) camera.vflip = True camera.hflip = True # OSS 初始化 auth = oss2.Auth(options['ak'], options['sk']) bucket = oss2.Bucket(auth, options['endpoint'], options['bucket']) # IOT Mqtt 初始化 client = iot.getAliyunIotMqttClient(options['productKey'], options['deviceName'], options['deviceSecret'], secure_mode = 3) client.on_connect = on_connect client.connect(host=options['productKey'] + '.' + options['host'], port=options['port'], keepalive = 60) while True: # 当高电平信号输入时报警 if GPIO.input(options['ir_pin']) == True: print " Someone is coming!" takephoto2oss(client) else: continue sleep(3) 3 测试运行 3.1 设备端运行 在py-demo文件夹下运行 python monitor.py 3.2 云端查看上送消息 进入设备界面,观察设备状态在设备的Topic列表中,也可以看到发布的消息个数高级版的产品,还提供了消息日志,而本例中的产品是基础版,并无此功能。 3.3 钉钉群机器人结果 当有人出入门口的时候,钉钉群就可以收到机器人的消息推送 3.4 后续完善 如果大家有兴趣,还可以进一步结合阿里云的人脸识别服务,再配合继电器,实现人员考勤和出入门禁的功能 总结 通过阿里云物联网平台,结合阿里云提供的其他产品和服务,使用者可以快速地构建一套基于云边端一体的IOT产品,开发者只需关注业务层面的开发,而不用再花太多的精力在底层和通讯上,大大减少了开发周期,实现了产品的快速研发和迭代,节约了开发成本。
2018年9月26日,阿里云ACE在厦门大学嘉庚学院举办了一场别开生面的学术交流会。本次交流会名为《Let's play IoT - 阿里云ACE厦门大学嘉庚学院技术沙龙》,是由阿里云ACE厦门同城会、阿里云MVP合作伙伴-厦门宏网信息技术股份有限公司携手厦门大学嘉庚学院信息学院共同举办。活动的主题是阿里云物联网生态与技术,交流会邀请了IOT技术专家、阿里云IoT合作伙伴的陈俊丰先生为大家介绍了阿里云物联网生态、平台框架,并通过现场操作实现一个简单的demo,快速地上手阿里云IoT平台。 26日下午两点,阿里云IoT学术交流会在公共教学楼101多功能厅正式开始。现场人潮涌动,将近400多师生到场。厦门大学嘉庚学院信息科学与技术学院副院长刘宝林教授主持了本次活动,厦门大学嘉庚学院副院长刘暾东教授、信息科学与技术学院院长夏靖波教授以及嘉庚学院的师生们参加了本次活动。 图:与会人员 图:刘暾东副院长致辞 陈俊丰介绍阿里云物联网生态系统架构及阿里云物联网平台Link Platform技术框架:结合云、管、边、端4个维度展示了平台提供的设备接入、设备管理、远程维护、规则引擎等强大功能。接着陈俊丰以一个实际demo,带领着师生们试验了15分钟内实现设备上云。大家在陈俊丰的指导下亲自动手实践,在短时间内完成了一个基本的物联网demo开发,在阿里云物联网平台上实现了终端设备状态的开发和采集,切身感受到了Link Platform的高效便捷。 图:陈俊丰先生分享 图:师生在陈俊丰先生的指导下实践操作 利用会中小憩,阿里云ACE厦门同城会会长黄晟用风趣幽默的语言向师生们介绍了阿里云ACE的理念与愿景,发展和现状,以及加入ACE组织后的权益。能够帮助有志于从事技术开发工作的工程师找到方向、提升能力并推荐优秀的开发者进入阿里云及其生态圈企业就职。通过现场的讲解,让师生们更为直观的了解阿里云ACE同城会。 图:会长介绍阿里云ACE组织 分享的过程中还设置了交流互动环节,同学们踊跃参与,将会场的气氛推向高潮。 图:师生现场互动 会后,厦门大学嘉庚学院的师生们都纷纷加入了ACE厦门同城会群,成为ACE的一员。通过本次活动,ACE厦门同城会掀开了在高校开展活动的一个新的篇章。它帮助即将工作的莘莘学子们制定好职业规划,为他们打开新技术之门、社会之门。
6月23日,阿里云在厦门举办了第十期阿里云MVP Tech Show,本次活动也是宏网信息与阿里云签署MVP项目城市合作伙伴协议之后的首次线下沙龙活动,活动主题是“大数据之美”。随着社会信息化的高速增长,各行各业每时每刻产生着大量的数据,这些数据如何去合理的存储与展现,是很多企业面临的问题,本期MVP Tech Show邀请了厦门点触科技股份有限公司研发经理上官成与美柚大数据专家李庆勇两位阿里云MVP为大家进行数据可视化与数仓建模最佳实践分享。 当天厦门骤雨大作,依然有90多位ACE冒着风雨赶来参加活动。在活动现场,上官成为大家分享了《日志聚合与可视化》,大数据的火热让大数据可视化炙手可热,在传媒、制造、医疗等行业领域,特别是一些监控中心、调度中心等重要场所,大屏幕显示系统已经成为信息可视化不可或缺的核心基础系统。 阿里云MVP上官成分享《日志聚合与可视化》 李庆勇分享的主题是《数据仓库建模之事实表设计》,数据既已成为生产资料,这就需要有一个数据仓库将这些数据存储起来,数据仓库建模就是数据组织和存储方法。 在两位MVP分享实践过程中,我们设置了问答环节,对于MVP提出的问题回答正确的ACE能获得阿里云提供的精美礼品。在分享结束后,ACE纷纷提出问题向两位MVP请教,现场气氛十分热烈,大家均表示受益匪浅。 ACE积极回答MVP的提问 在活动的最后,阿里云ACE厦门同城会会长黄晟与两位MVP共同抽取了4位幸运ACE,并送上由阿里云提供的精美礼品。随着本次活动的圆满落幕,大家都意犹未尽,期待下一次阿里云ACE线下沙龙活动。 阿里云MVP与四位幸运ACE 阿里云ACE厦门同城会会长黄晟为大家答疑 集体合影,活动结束,期待下次再会!
3月24日下午,阿里云ace厦门技术分享沙龙在厦门高新区的创新驿站举办。 本此活动,虽说是ace厦门同城会的第一次室内活动,但由于前期的充分准备,现场吸引了近40位小伙伴前来参加,不仅有老ace,更有新成员的加入,甚至有个伙伴趁着来厦门出差,也赶场参加,场面非常热闹。活动邀请了阿里云2位MVP给ace们做了精彩的分享,旨在能帮助厦门本地及周边更多的开发者、技术爱好者能更快、更好的得到最新的技术分享和成长滋养。同样令人惊喜的是,另一位mvp白宦成,利用出差之便,特地前来助阵交流,阿里云福建分公司的涂传斌老师也难得能在百忙中抽空参加活动。内容丰富干货满满,一顿技术的饕餮盛宴等待着大伙品尝。咱们先来看看现场的图片吧。 阿里云MVP是来自世界各个领域的技术精英,是技术的爱好者和传播者,希望帮助全球的广大开发者更好的使用云计算,从先行者的角度进行分享、布道,从而影响和指引更多的开发者。 首先上场的是阿里云mvp上官成,他分享主题是《移动网络游戏微服务演进之路》。做为资深的架构师和开发经理,上官成从实际工作经验中出发,详细地介绍了移动网络游戏从单体架构、微服务架构、开发运维一体化、到服务网格的逐步演变过程,细致地分析和总结了各阶段的优缺点。 接着,阿里云MVP黄晟分享了《初识流计算与阿里云流计算实战》。流计算是一种特殊的增量计算。它利用分布式的思想和方法,对海量“流”式数据进行实时处理。它源自业务对海量数据,在时效价值上的挖掘诉求。黄晟通过静态数据、流数据的概念引入了流计算,深入浅出地介绍了流计算的由来、概念以及常用的流计算框架。正值阿里云流计算产品刚刚正式发布,通过现场的实战操作,模拟了实时统计电商平台销量统计这样的业务场景,核心介绍了阿里云流计算核心StreamSQL。在现场,用了仅不到半小时,搭建了一个从数据采集到流计算,从流计算到数据可视化的一整套的流计算解决方案,小伙伴们都惊呆了有木有,原来这么高大上的流计算原来在阿里云上的实现竟如此快速和便捷。 两场精彩分享过后的互动环节,大伙们对今天的话题进行了广泛、深入的讨论。服务网格的实现,流计算的适用范围等都是大家热烈讨论的话题,两位MVP分别做了现场答疑。涂老师这时候也当仁不让,补充了自己的观点,并抛砖引玉,给大家带来更多的思考。 最后,大家都满意地说,这样的线下分享沙龙应当经常举办,并表示非常期待下次的活动。三位MVP的合影,从左到右分别是:上官成、黄晟、白宦成。请注意他们的服装 此次活动特别感谢厦门宏网信息技术有限公司,提供了场地、茶位费、点心水果,还有。。。。美女主持人和负责签到的妹子,很惊喜有木有~~~没图说个xx 不过,万事皆无完美,唯一小小遗憾是:拜托,负责摄影的哥们,能不能给点人物特写。。。。。。
在那冬日暖暖的阳光下,1月27日早上9点钟,阿里云ACE厦门同城会春节活动在美丽的厦门软件园二期中心公园顺利开展起来啦~~~ 这是阿里云ACE厦门同城会举办的第一次线下活动,来的帅哥美女还真不少! 虽说我们这群IT男女,平时貌似很宅,刚开始相互之间还有点陌生,但通过几轮小游戏,大家很快就熟悉了起来。 别看我们平时面对的都是脑力活动,但真正的运动项目较量起来,还真是当仁不让。几位美女更是巾帼不让须眉,在跳绳等项目发挥了巨大的优势! 废话不多说,看看下面的VLog,瞧瞧这帮伙伴们,多带劲: 快乐的时间总是很短暂,随着更多、更优秀的成员的加入,相信下期活动会更有趣,更精彩!
优化的核心原则 由于 DRDS 是一个分布式关系数据库服务,处理的是分布式关系运算。分布式无疑会带来额外的跨库网络开销,而大家都知道,网络通信的延迟比单机内通信的延迟大得多。因此分布式环境中优化更应侧重考虑: 减少网络传输; 减少 DRDS 计算量,尽量将计算下推到下层的数据节点上,让计算在数据所在的机器上执行; 充分发挥下层存储的全部能力。 数据库表创建优化 数据库表的创建优化是最基本的数据优化,是需要在数据模型建立时就需要确定数据的存储、分片和路由的方式。如果模型设计的不够科学合理,后期通过应用的 SQL 来优化,都是成效甚微的。特别是对 DRDS 分布式数据库而言,一旦数据表建立了,后期的分库分表的拆分方式是无法进行修改的,即便数据表删除重建,数据的恢复相对麻烦。 在 DRDS 的数据库表中,主要存在有以下几种形式: 单库单表 对于数据量不大的数据表,可以如同普通的单库 RDS 表一样,建立单库单表。但由于 DRDS 上单库单表只存在 0 库上,和其他不在 0 库的表可能存在跨库 JOIN 的风险。此外,过多的单库单表,容易造成 DRDS 后端分库资源损耗不平衡,0 库的 IO 消耗过大的问题,应尽量予以避免。 小表广播 对于数据量少,且数据变化不频繁,数据一致性要求不高的单库单表,为了解决上述的跨库问题,可以考虑使用小表广播。小表广播是指将表复制到每个分库上,在分库上通过同步机制实现数据一致,但存在秒级延迟。好处在于,可以将 JOIN 操作下推到底层的分库,来避免跨库 JOIN,提高执行效率。如下所示,建表时使用BROADCAST关键字: CREATE TABLE users ( user_id int, user_name varchar(50), create_time date, primary key(id) ) ENGINE=InnoDB BROADCAST; 除了使用命令,也可以在 DRDS 控制面板上指定广播表。但需要额外注意,小表广播的使用限定: 表数据量少,尽量不应超过 10 万; 数据的更新不能太频繁;过大的表和更新过于频繁,都容易增加 DRDS 数据库底层 IO 压力和网络消耗。 分库分表 DRDS 在后端将数据量较大的数据表水平拆分到后端的每个 RDS 数据库中,这些拆分到 RDS 中的数据库被称为分库,分库中的表称为分表。拆分后,每个分库负责每一份数据的读写操作,从而有效的分散了整体访问压力。而分库分表优化的目的在于,重点减轻分布式环境中的网络 IO 开销,尽量将 SQL 中的运算下推到底层各个分库执行,从而减少网络 IO 开销、提升 SQL 执行效率。 如何确定是否分库分表 一般情况下,单个物理分表的容量不超过 500 万行数据。我们通常可以预估未来的数据增长量,用估算出的总数据量除以总的物理分库数,再除以建议的最大数据量 500 万,即可得出每个物理分库上需要创建的物理分表数。阿里公有云环境上,每个 RDS 实例上默认会创建 8 个物理分库,因此公式如下: 物理分库上的物理分表数 = 向上取整(估算的总数据量 / (RDS 实例数 * 8) / 5,000,000) 如果取得的分表数<=1,可以考虑只分库,不分表。如果>1,则除了分库外,最好在各物理分库上进行分表。 拆分方式 要实现数据表的分库分表的拆分,主要包括 2 个方面,即拆分键和拆分算法(函数)。例如,以下建表语句中对数据表做了分库处理: CREATE TABLE users ( user_id int, user_name varchar(50), create_time date, primary key(id) ) ENGINE=InnoDB dbpartition by hash(id); 其中 dbpartition by 指的是需要分库,hash(id) 指的是分库的拆分键是 id 字段,分库使用的拆分算法是 hash。 选择拆分键的原则 拆分键即分库/分表字段,DRDS 根据拆分键的值将数据表水平拆分到每个 RDS 实例上的物理分库中。 DRDS 目前支持数字型、字符型、日期型的字段做拆分键; 尽可能找到数据表中的数据在业务逻辑上的主体,并确定大部分(或核心的)数据库操作都是围绕这个主体的数据进行,然后可使用该主体对应的字段作为拆分键; 根据数据分布和访问的均衡度来考虑拆分键,尽量将数据表中的数据相对均匀地分布在不同的物理分库/分表中; 拆分键的合理选取,对于数据表的性能,起着至关重要的作用。如果未能科学合理的选取拆分键,可能造成: 数据分布不均匀,各底层 RDS 库的 IO 无法有效分摊,造成有的分库空闲,有的分库繁忙; 上层应用查询处理数据时,无法带上拆分键,造成大量数据汇总和计算需要由底层各节点 RDS 汇集到 DRDS 层,大大增加网络 IO 开销和 DRDS 内存资源损耗。同时由于查询执行时间加长,容易造成 DRDS 数据库连接池无法及时释放; 过多的跨库语句也造成增加应用中跨库事务处理的复杂性,降低了系统 TPS。 拆分算法的选择 拆分算法决定了分片数目与路由算法。目前常用的拆分算法主要有: 哈希函数,常用如 HASH,UNI_HASH; 日期函数,常用如 MM,DD,WEEK,YYYYMMDD等; 双字段哈希函数,RANGE_HASH。除了 RANGE_HASH 可以同时用2个字段外,其他拆分算法都只支持单字段。 当数据表通过上述的拆分键、拆分算法被拆分为多个分库分表时,数据在分库分表的分布规则就固定了。但是通常数据的业务使用场景非常复杂,如果数据的查询纬度和数据拆分分布的规则一致,单条 SQL 会在一个分库分表上执行;如果数据的查询使用纬度和数据拆分分布的规格不一致,单条 SQL 就很有可能在多个分库分表上执行,出现跨库查询,跨库查询会增加网络 I/O 的成本,查询效率必然下降。 这时候可使用拆分算法RANGE_HASH和异构索引来实现。 RANGE_HASH 双字段哈希函数 RANGE_HASH 与其他拆分算法不同,可设定2个拆分键,是根据任一拆分键后 N 位计算哈希值,然后再按分库数去取余,完成路由计算。N 为函数第三个参数。例如:RANGE_HASH(COL1, COL2, N) ,计算时会优先选择 COL1,截取其后N位进行计算。 COL1 不存在时找 COL2。适用于需要有两个拆分键,并且查询时仅有其中一个拆分键值的场景。但有以下约束: 两个拆分键的类型必须是字符类型或数字类型; 两个拆分键皆不能修改; 插入数据时如果发现两个拆分键指向不同的分库或分表时,插入会失败。RANGE_HASH 只能解决2个查询维度的问题,超过 2 个以上的,则需要使用 异构索引 。 异构索引 异构索引 的本质实际采用的方式就是用“空间换效率”的方案,也就是将同一份数据表,冗余存储多份,按照不同的业务使用场景进行拆分,保持拆分纬度和使用纬度统一,而多份数据之间会实时数据复制以解决数据一致性问题。 使用异构索引主要注意的是: 过多的异构索引表会影响同步效率,对源数据表造成同步压力,同时增加数据库底层 IO 压力; 异构索引表之间的同步存在秒级延迟,对实时数据一致性要求较高的存在风险; 异构索引数据的修改更新只能通过主表进行,索引表只读。目前公有云上的异构索引仍处于内部测试阶段,建立必须通过后台工单的方式。 建表语句中的自增长 在我们建数据表时,经常有使用到自增长的字段类型,有时候也拿来做表的ID主键。在传统的RDS单数据库中,将某个字段设置成自增长,可以实现该字段的全局唯一的单调连续递增。但DRDS将数据库表进行了分库分表,是否能够保证此自增长字段也具有上述特性呢。事实上,在 DRDS 中,当拆分表和广播表的字段指定了 AUTO_INCREMENT 后,DRDS 就会创建隐式的 Sequence 来生成全局唯一和有序递增的数字序列。注意 ,非拆分表的 AUTO_INCREMENT 的值是由底层 RDS 自己生成的。目前 DRDS 的 Sequence 有 3 种类型:GROUP(默认)、TIME、SIMPLE。语法如下: CREATE TABLE <name> ( <column> ... AUTO_INCREMENT [ BY GROUP | SIMPLE | TIME ], <column definition>, ... ) ... AUTO_INCREMENT=<start value> 需要注意的区别是: GROUP,是系统默认的,大部分场景下建议选用。全局统一,但非连续的非单调递增的(整体来看),不能循环。优点,速度快,不存在性能问题; TIME,只能用于 BIGINT 类型字段,数值完全随机,全局统一,性能高; SIMPLE,最接近于传统 RDS 的自增长序列,全局唯一、连续、单调递增。但是由于每次产生都需要持久化,效率较低,容易造成性能瓶颈,除非特定业务强制性要求,否则请谨慎使用。 SQL优化 DRDS 对 SQL 的优化方法与单机关系数据库有所不同,侧重考虑分布式环境中的网络 IO 开销,应尽量将 SQL 中的运算下推到底层各个分库执行,避免全库扫描,从而减少网络 IO 开销、提升 SQL 执行效率。 本文侧重DRDS的优化,并不等于原有单库的SQL优化策略就不适用。当最终SQL语句下推到下层RDS分库节点执行时,原有MYSQL的优化策略也是起效的;下面介绍的有些DRDS的SQL优化策略是与单机RDS(MYSQL)是完全相同的。 基于上述核心原则,可以有以下优化方向: 条件优化 所谓条件优化,就是优化 SQL 语句 RUD 中 WHERE 中过滤条件的优化,包括了 SELECT、UPDATE、DELETE。 尽量包含拆分键基本原则是,过滤条件中应尽量包含带有拆分键的条件,可以让 DRDS 根据拆分键对应的值将查询直接下推到特定的分库,避免全表扫描。经测试,在 10 万记录的分库表中,过滤条件中含有拆分键的比不含拆分键的,速度秒数至少提高3个数量级。 含拆分键的过滤条件的取值范围越小,速度越快。例如,执行消耗时间上的对比 = < 区间 < 子查询 以上区间的记录条数<子查询记录条数经常有些时候,查询很难明确目前拆分键的值,这时候,可能需要中间表或存储进行转换。例如,查询A表时,过滤条件并不能明确A表的拆分键值或者范围,这时候可先从 B 表(或 REDIS 存放)查出相应的拆分键值或者范围,然后在叠加到 A 表的过滤条件中,以提高语句执行效率。 避免类型转换和列运算WHERE 条件中使用类型转换和列运算,一方面会导致分库中索引无法使用,进行全表扫描,另外也会增加 DRDS 的计算负担。例如: SELECT order_id FROM orders WHERE YEAR(create_time)<2017; 应改成: SELECT order_id FROM orders WHERE create_time<’2017-01-01’; 用 NOT EXIST 替换 NOT INNOT IN 无法使用索引,会进行全表扫描,将加重分库的负担。 尽量少排序如果业务上无强制排序要求,尽量不要进行 ORDER BY 排序。如果用的话,最好用在带有索引的列上,或者考虑在应用中进行排序。因为在 DRDS 中,各分库的排序必然是需要推到各个分库中排序并将结果汇集到DRDS内存中进行计算。 数据量优化 基本原则,查询交互和返回的数据量应保持尽量少。 字段补全SELECT * 这种未明确指定返回字段的 SQL 语句应尽量避免。只把需要的字段返回回来,这样不仅可以加快数据读取速度,同时也可以减轻网络 IO 的消耗; 返回记录限定有时业务仅需要返回一条记录,就可以使用 LIMIT 1 来终止数据库引擎继续扫描整个表,减少数据库IO损耗; 少用UNION可以考虑使用 UNION ALL 替代 UNION。UNION 和 UNION ALL 的差异主要是前者需要将两个(或者多个)结果集合并后再进行唯一性过滤操作,这就会涉及到排序,增加大量的 CPU 运算,加大资源消耗及延迟。所以当我们可以确认不可能出重复结果或者不在乎重复结果的时候,尽量使用 UNION ALL 而不是 UNION。目前,貌似 UNION 在 DRDS 中等于 UNION ALL,尚不具有去重功能; LIMIT优化我们在数据清单查询时,经常使用到分页,底层通过 SQL 语句携带 LIMIT 关键字来实现指定游标记录的返回。DRDS 在执行带有 LIMIT [ offset_, ] _row_count 的查询时,实际上是依次将 offset 之前的记录读取出来并直接丢弃,这样当 offset 非常大的时候,即使 row_count 很小,也会导致查询非常缓慢。例如: SELECT * FROM users LIMIT 10000000,10; DRDS 实际上的操作是,从各分库中读取0~10000010的记录,汇集到上层后,再将多余的记录丢弃掉,导致了产生大量无用网络IO。针对上述情况,SQL 优化方向是先查出 ID 集合,再通过 IN 匹配真正的记录内容: SELECT * FROM users WHERE user_id IN (SELECT user_id FROM users LIMIT 10000000,10); 这种优化当返回的_offset_很大,且字段数比较多的时候,效果尤为明显。 关联优化 原则上,应尽可能将 JOIN 查询优化成能够在分库上执行的可下推的 JOIN 形式。如果一个 JOIN 查询不可下推(即 JOIN 条件和过滤条件中均不带有拆分键),则需要由 DRDS 完成查询中的部分计算。正如上述条件优化所述,各关联表的过滤条件中应尽量包含拆分键,有利于将查询下推到特定的分库,避免全表扫描。更进一步,如果能达到关联各表中所带的分库拆分键是相同的,则能保证在同一物理分库中,则效率更高。 单库单表形成广播表小表形成广播表后,当同分库中的表进行 JOIN 时,语句就可以下推到分库中执行,提高执行效率。 子表查询优化应尽量改成 JOIN 的形式。通过 EXPLAIN 可以查看到子查询是一个相关子查询( DEPENDENCE SUBQUERY )。分库是先对外表A执行全表查询,然后根据 ID 逐次执行子查询,如果外层表是一个很大的表,我们可以想象查询性能会表现比这个更加糟糕。 JOIN 优化应尽量将数据量较小的表作为驱动表,也即 LEFT JOIN 的左表。如果有过滤条件应当尽量放到驱动表的过滤条件中,将驱动表的数据集尽可能缩少。在此优化规则中,如果驱动表很小,关联表很大,那么优化后的效率将会提高很多。 例如: SELECT * FROM a LEFT JOIN b ON a.id=b.ref_id WHERE b.ref_id>10; 可以优化为: SELECT * FROM (SELECT * FROM a WHERE id >10) a LEFT JOIN b ON a.id=b.ref_id; 关联表个数限定建议不能下推到同一个物理分库的 SQL 语句中,表关联尽量不要超过 2 个。超过 2 个以上的语句,可考虑在应用中拆分成多次查询。 合理利用HINT 用 HINT 限定读取只读实例当 DRDS 配置了读写分离时,应用会自动根据配置的读权重来透明地选取是从主实例还是只读实例中读取数据。 而通过显式地在 SQL 语句中使用 HINT,可指定语句是在主实例上执行还是在只读实例上执行。如下所示,指定了只在只读实例中执行: /!TDDL:SLAVE*/ SELECT * FROM users; 好处在于,对于应用中数据一致性要求不高的,个别资源消耗较大的或者执行时间较长的 SQL 语句,通过显式指定在只读库上执行,可以避免分配到主实例库上,消耗主库的资源。 定义SQL语句执行超时时间在 DRDS 中,DRDS 节点与 RDS 的默认的 SQL 执行超时时间是 900 秒,但是对于某些特定的慢 SQL,其执行时间可能超过了 900 秒 。针对这种慢 SQL,DRDS 提供了调整超时时间的自定义 HINT。通过这个自定义 HINT 可以任意调整 SQL 执行时长。如下所示,设置语句超时时间 30 秒: /!TDDL:SOCKET_TIMEOUT=30000*/ SELECT * FROM users; 注意:超时时间设置得越长,占用数据库资源的时间就会越长。如果同一时间长时间执行的 SQL 过多,可能消耗大量的数据库资源,从而导致无法正常使用数据库服务。 索引优化 在索引字段尽量不要出现空值,这样将导致用不到索引,可以考虑建表时字段增加默认值以避免空值出现; 创建二级索引时,最好创建在查询相对多,更新相对少的字段上; 创建联合索引要注意字段排列顺序,把过滤最多记录数的字段放在第一位,以此类推; 目前 DRDS 中,PK 和 UK 只能保证在某个分库(分表)是唯一的,并不能保证全局。如果要做到全局唯一,必须同时还是分库键才行 配置读写分离 通过配置 DRDS 读写分离功能,能有效对主实例的读流量进行分流,减轻 RDS 主实例的读压力。同时,从另一个角度来说,从只读实例上进行读取时,也避免了主实例写库时事务锁造成的数据库 IO 问题,加快了数据的读取速度。由于DRDS 的读写分离功能是对应用透明的设计,应用可以在不修改任何代码的情况下,调整读权重,即可将读流量按配置的比例在主 RDS 实例与多个只读实例之间进行分流;写流量则全部到主实例,不做分流。 优化常用指令 工欲善其事,必先利其器。DRDS为我们提供了一系列的指令,便于我们跟踪数据库表的创建和SQL语句执行情况,方便进行调优。以下是比较常用的指令: 查看数据表存储 使用 SHOW CREATE TABLE 指令检查表结构; 执行 SHOW TOPOLOGY FROM TABLE_NAME 指令获取表拓扑结构,例如分成了几个库几个表等; 使用 SHOW FULL RULE查看数据库下逻辑表的拆分规则。 查找慢SQL 使用 SHOW PROCESSLIST 指令观察所有当前执行的 SQL 状态; 使用 SHOW FULL SLOW 指令显示历史逻辑慢 SQL,即应用发送到 DRDS 的 SQL; 使用 SHOW FULL PHYSICAL_SLOW 指令显示历史物理慢 SQL,即 DRDS 发送到 RDS(MySQL) 的 SQL。 查看语句执行计划 使用 EXPLAIN SQL 指令查看指定 SQL 在 DRDS 层面的执行计划。可观察到SQL语句在DRDS中的分片情况和路由算法; 使用 EXPLAIN EXECUTE SQL 指令查看底层存储的执行计划,等同于 MYSQL 的 EXPLAIN 语句。可观察到SQL在各分库中如何执行语句操作及索引使用情况; 搭配使用 TRACE SQL 和 SHOW TRACE 指令可以查看具体 SQL 的执行情况,需要注意,使用TRACE,SQL语句会真正执行。 后记 路漫漫其修远兮,吾将上下而求索。性能优化是一个庞大、复杂而又细致的工作,不可能一朝一夕一蹴而就,范围可涉及到硬件、网络、应用、存储等等层面,绝不能脱离整体而片面、孤立地看待问题。本文针对分布式关系型数据库 DRDS 的优化做了一些入门简要介绍,仅仅是冰山上的一角,希望借此能帮助大家快速入门,能逐步挖掘释放 DRDS 效能,扬长避短。 文中部分内容借鉴了阿里官网DRDS的用户指南,更多的实例大家可以参阅官方文档。内容如有不正确、不合理的地方,也欢迎大家批评指正。
2020年12月