2024年3月

一、背景

爱番番大前端整体面临以下问题:

  1. Metrics:URL的RED指标不全。URL不全,ERROR缺失,Duration分位置缺失。整体实效性为T+1。无法及时感知问题。只对基本页面级别的读操作进行了监控。
  2. Tracing:Trace无法全端串联,直接影响具体case的跟进。无前端Trace。
  3. Logging:无Log。Sentry的Error信息目前仅PC接入。且未进行实际使用;和trace无法打通。
  4. 报警:对于异常数据缺乏有效报警。

二、目标

  1. Metrics目标:RED指标,自定义Metrics
  2. Tracing目标:全端单动作追踪
  3. Logging目标:Error级别Log具备。单Traceid和Log能够实现串联

2.1 核心目标

从全局问题出发,能够洞察统计性的页面url真实RED指标,以及可以做操作流任意阶段之间的统计性耗时分析。从个案问题出发,能够基于用户id进行任意一次全端调用链追踪。
具体地:
  1. 页面级别性能监控。可包含条件检索和正常页面刷新。包含RED
  2. 能够基于用户id进行任意一次全端调用链追踪。
  3. 可分析一个操作流任意阶段之间的统计性耗时。
  4. 准实时呈现。数据延时小于5min。
  5. 可区分来源、地域、设备等核心信息
  6. 写请求性能监控。包含新建、编辑、删除。包含RED

2.2 目标抽象

本质上,抽象为
AggrEvent、Event、Trace、Span
四个概念。Transaction用来做纯埋点,此处暂不需要。
  1. 带TraceID(可被覆盖)和TimeStamp信息的Event
  2. 全端Trace下的调用链。本质上是前端的Event链路 + 服务端的Span链路串联而成。
  3. 考虑到批量传输性能。采用AggrEvent进行聚合发送。

三、 名词解释

  1. RED:
    RED方法是Weave Cloud在基于Google的“4个黄金指标”的原则下结合Prometheus以及Kubernetes容器实践,细化和总结的方法论,特别适合于云原生应用以及微服务架构应用的监控和度量。主要关注以下三种关键指标:
    1. (请求)速率:服务每秒接收的请求数。
    2. (请求)错误:每秒失败的请求数。
    3. (请求)耗时:每个请求的耗时。
  2. optid
    (operate-id):一个操作唯一对应的一个id。比如一次刷新,一次修改。
  3. reqid
    (request-id):单次请求唯一对应的一个id。如果重试,则reqid设置为新id。一个optid可能会存在多个reqid。
  4. tid
    (tracing-id):贯穿服务端端调用链的唯一id。
  5. optType: 某一类操作定义。比如一次刷新,一次修改。

四、目标拆解

4.1 Why

4.1.1 为什么要做大前端监控体系?

获取用户行为以及跟踪产品在用户端的使用情况,并以监控数据为基础,指明产研优化的方向。

4.1.2 收益是什么?

  1. Metrics:对各端各页面各场景RED指标准实时分析、可视化呈现,打破前端监控黑盒
  2. Tracing:打通前后端调用链,能够针对任意case进行前后端链路分析,让前端性能优化有的放矢
  3. Logging:对各端运行时错误进行实时监控报警,能通过日志最大程度还原用户现场并定位问题,提升前端页面稳定性

4.2 What

4.2.1 监控什么端?

爱番番现有产品各端:web,h5(浏览器、webview),Android,iOS,客户端,小程序

4.2.2 监控什么方向

(1) 数据监控

数据监控,顾名思义就是监听用户的行为。常见的数据监控包括:
  • PV/UV:PV(page view),即页面浏览量或点击量。UV:指访问某个站点或点击某条新闻的不同IP地址的人数
  • 用户在每一个页面的停留时间
  • 用户通过什么入口来访问该网页
  • 用户在相应的页面中触发的行为
统计这些数据是有意义的,比如我们知道了用户来源的渠道,可以促进产品的推广,知道用户在每一个页面停留的时间,可以针对停留较长的页面,增加广告推送等等。
该方向主要为业务服务,目前职责定位更符合神策埋点系统。

(2) 性能监控

性能监控指的是监听前端的性能,主要包括监听网页或者说产品在用户端的体验。常见的性能监控数据包括:
  • 不同用户,不同机型和不同系统下的首屏加载时间
  • 白屏时间
  • http等请求的响应时间
  • 静态资源整体下载时间
  • 页面渲染时间
  • 页面交互动画完成时间
这些性能监控的结果,可以展示前端性能的好坏,根据性能监测的结果可以进一步的去优化前端性能,比如兼容低版本浏览器的动画效果,加快首屏加载等等。

(3) 异常监控

产品的前端代码在执行过程中也会发生异常,因此需要引入异常监控。及时的上报异常情况,可以避免线上故障的发上。虽然大部分异常可以通过try catch的方式捕获,但是比如内存泄漏以及其他偶现的异常难以捕获;
爱番番目前采用sentry进行错误日志采集。

4.2.3 监控什么内容

1.web
类别
内容
页面分析
具体丰富的页面指标,提供如服务器端响应时间、网络延时、DOM解析和页面渲染时间等性能指标
帮助研发更快捷的定位服务端、客户端的页面问题
Ajax请求
获取用户访问过程,页面发出的所有Ajax请求URL、引用页面URL,监控某一Ajax请求的响应时间、回调时间、上传数据量、下载数据量以及响应过程中服务器返回的错误
JS error
代码级定位出错页面或者脚本URL,引用页面URL,出错的行列信息、堆栈等信息,通过sourceMap定位源码文件,通过pageId,operateId以及tid最大限度还原上下文
监测Web App中JS错误的数量,各浏览器出错百分比和JS错误率随时间变化的趋势
浏览器监测
统计不同版本浏览器和浏览器类型的平均页面加载时间和吞吐率
提供基于多平台浏览器性能分析,兼容性分析
慢页面追踪
抓取加载时间超过设定阈值的页面上的元素信息,及每个元素的TCP建连、首包及剩余包等所需时间
详细定位页面上的哪些元素的加载拖慢了页面的响应,为优化用户体验提供依据
2.其他端:
h5(浏览器、webview)
统计从端点击开始,到指定页面渲染全流程时间分布,包括(容器耗时,框架耗时,网络耗时,渲染耗时等)。
统计NPJS框架自身稳定性,以及各个阶段耗时。
统计任意多个操作各个时间断耗时。
iOS,Android

现在使用百度移动端性能中台,功能基本满足爱番番监控、报警功能;
移动端监控指标主要为 卡顿和崩溃,目前有崩溃堆栈信息日志;暂无交互时长分析(xray平台有,手百性能中台没有)
PC客户端

electron监控方案参考WEB版

四、方案调研

自建or接入其他平台?

业务埋点采买神策。
性能监控平台:
厂内:
目前日志中台可以接入日志,但是需求是前端页面的性能指标,属于性能平台范围,目前性能平台无前端性能指标建设。
性能中台的定位主要是针对native,当前通用能力主要是崩溃、卡顿、端异常、Flutter异常、日志回捞等,其他的一些能力主要在手百、或者相关SDK上,还未对外输出。经沟通暂不考虑支持前端性能监控。
厂外:性能监控主流收费平台:
收费平台功能大同小异,均能满足前端基础性能监控需求。
优点:有较为成熟的解决方案,能快速满足多端性能监控基本需求;
缺点:
  1. 收费
  2. 无法与部门现有后端APM体系打通
  3. 无法满足特定case分析需求

结论:复用神策埋点SDK存储,上报能力及通路,进行SDK二次封装;自建日志服务及展示系统。

五、方案设计 (web)

1、采集

(1)埋点SDK
基于神策SDK进行二次封装
统一封装埋点SDK,通过npm包形式进行版本管理;使用方在公共模块对埋点SDK进行初始化。
增加无侵入性能采集能力,提供采样率配置等可配置扩展能力。
(2)通用统计指标(参考听云)
指标
统计方式

白屏

用户浏览器输入网址后至浏览器出现文字或1px图片所花费时间。计算规则:优先使用Chrome、IE提供的firstPaintTime,没有获取到计算head中link、script脚本下载的最长时间。

首屏

用户浏览器首屏内所有的元素呈现所花费时间。计算规则:寻找首屏区域内的所有图片,计算最长加载时间得到首屏时间。

可交互

功能可以使用的时间,也指domready时间。计算公式:可交互=Navigation Timing API domContentLoadedEventStart – fetchStart。

完全加载

页面完全加载总时间。指从NavigationStart事件开始到LoadEventEnd事件结束,计算公式:LoadEventEnd-NavigationStart

HTML加载

指主HTML文件从DNS解析到加载完且不包含排队时间和应用服务器响应时间,即包含DNS,TCP建连,Request和Response,计算公式:responseEnd-domainlookupStart-排队时间-应用服务器响应时间

页面渲染

指从responseEnd事件开始到loadEventEnd结束,包含DOM解析和资源加载,计算公式:LoadEventEnd-responseEnd

DOM解析

指从responseEnd事件开始到DomContentLoadedEventEnd事件结束,计算公式:DomContentLoadedEventEnd-responseEnd

资源加载

指从DomContentLoadedEventEnd事件开始到loadEventEnd事件结束,计算公式:loadEventEnd-DomContentLoadedEventEnd

JS错误率

出现JS错误的比例。JS错误包含Javascript错误代码和位置信息。

服务端响应时间

服务器响应时间是指应用服务器处理请求所消耗的时间,即应用响应时间,等于请求到达应用服务器到应用代码执行完成并输出响应信息的时间。(需要通过Server探针自动注入方式嵌码,否则服务器响应时间为零)

AJAX请求响应时间

所有Ajax请求时间在时间轴的投影合并的总耗时

unload

卸载当前页面的耗时,计算公式:unloadEnd-unloadStart

Redirect

页面重定向操作所消耗的时间,计算公式:redirectEnd-redirectStart

Cache

取缓存数据的耗时,计算公式:domainLookupStart-fetchStart

DNS

通过域名解析服务(DNS),将指定的域名解析成IP地址的消耗时间。

TCP建连时间

浏览器和WEB服务器建立TCP/IP连接的消耗时间。当元素下载完成后,浏览器可能会根据服务器返回的结果保持此连接,而不是完全关闭此连接。当监测节点再次和相同的服务器建立连接时,会复用此连接,对应消耗时间可能为0。此指标即为TCP/IP连接三次握手的前二次握手的时间(从IE发送TCP包SYN到收到服务器返回的TCP包SYN ACK的时间),第三次握手时间(从IE发送TCP包ACK到服务器接收此TCP包的时间)不计算在内。

排队时间

排队时间指服务器端的请求阻塞时间,即请求从Web前端服务器(例如Apache, nginx或F5负载均衡设备)到达应用服务端的时间。

首包时间

从开始页面请求到浏览器开始接收到HTML代码的时间,不包括排队时间和服务器端的时间,计算公式:responseStart-connectEnd -排队时间-服务器响应时间

剩余包时间

从responseStart事件开始到responseEnd事件结束,计算公式:responseEnd-responseStart

首次渲染时间

从导航到页面首次渲染消耗的时间,计算公式:firstPaintTime-navigationStart(又名:白屏时间,firstPaintTime)

首次交互时间

从用户的第一个动作发生时间 – navigationStart,其中动作包括:点击,按键,滚动鼠标。

自定义加载时间

(用户可感知时间)
每个页面都可以设置一个用户自定义的加载时间性能指标。计算方式为路由切换时间至主动调用sdk ready方法时间差值

AJAX平响时间

平均每次AJAX请求的响应时间

AJAX传输数据量

单位KB,平均每次AJAX请求的数据传输量(上传+下载字节数)

AJAX回调时间

平均每次AJAX请求的回调时间(回调时间是指当数据从服务器传到客户端之后,本地代码调用这些数据做相应的处理,可以理解为本地执行时间)

客户端时间

从请求某资源到下载完过程中,没有出现网络传输的时间片段之和,比如DNS-TCP建连,之间的切换需要消耗CPU来调度,这就可能会产生很短的时间空隙

事件平均响应时间

操作请求完成时间。
统计方式,通过performance API

Core Web Vitals(
https://web.dev/vitals/

核心指标
metrics
描述
含义
TTFB
time to first byte
从请求到数据返回第一个字节所消耗时间
TTI
DOM树构建完毕,可绑定事件
DCL
DOMContentLoaded
HTML文档完全加载解析完成
L
onLoad
依赖资源全部加载完毕
FP
first paint
第一个像素点绘制完成时间
FCP
首次绘制非空白节点时间
FMP
first meaningful paint
首次有意义绘制(需要自定义)
LCP
在视口中最大的页面元素加载时间
FID
用户首次和页面交互到页面响应的时间
CLS
度量在页面开始加载到其生命周期状态更改为隐藏之间发生的所有意外布局更改的累积分数
使用谷歌
Web Vitals进行获取

interface Metric {
  // The name of the metric (in acronym form).
  name: 'CLS' | 'FCP' | 'FID' | 'LCP' | 'TTFB';

  // The current value of the metric.
  value: number;

  // The delta between the current value and the last-reported value.
  // On the first report, `delta` and `value` will always be the same.
  delta: number;

  // A unique ID representing this particular metric that's specific to the
  // current page. This ID can be used by an analytics tool to dedupe
  // multiple values sent for the same metric, or to group multiple deltas
  // together and calculate a total.
  id: string;

  // Any performance entries used in the metric value calculation.
  // Note, entries will be added to the array as the value changes.
  entries: (PerformanceEntry | FirstInputPolyfillEntry | NavigationTimingPolyfillEntry)[];
}

(3)前后端调用打通
    1. pageGuid,重新定义pageGuid生成规则,暂定为 router+orgId+timestamp+random ? 进行加密处理
    2. requestId,由网络库统一生成,生成方式参考BFE_logId生成规则,在请求header中携带,用于skywalking tid进行映射
    3. operateId,需要使用者明确【操作】起止时机,并手动调用SKD API。

      1) 在使用方初始化【操作】时
      ,调用 SDK.initOpt('
      操作名
      '),该方法返回本次操作
      operate
      Name
      ,用于后续传递;
      1) 在使用方明确【操作】开始时,调用SDK.startOpt('operateName'),SDK生成唯一operateId,存于localStorage.pageGuid..operateName.operateId(示例);
      2) 为了保证上下文独立性,在该操作涉及到的请求options中配置{operateName: 'operateName'},网络库在发送请求时,主动获取operateId并携带,取值方式为:localStorage.pageGuid..operateName.operateId || ‘’;
      3) 在使用方明确【操作】结束时,需调用SDK.endOperate(操作名);SDK将进行该次操作相关操作时间,页面渲染时长等统计,存储在localStorage.pageGuid.bucket中,通过上报策略时机进行统一上报。

(4)错误日志报警

现有sentry在采集错误日志时,增加pageGuid,requestId,operateId相关信息,通过(3)映射方式与Skywalking 调用链进行链路打通。
鉴别有用报错信息分类,确定报警阈值及报警人配置,进行HI,邮件,短信,电话报警方式;增加报警升级策略。
增强报表展示能力,通过pageGuid等维度查看页面级别报错,以便于进行下一步排查和跟进。

2、上报

直接复用神策SDK上报机制。
1.
上报时机(备选)
页面性能数据(数据量较少)
  • 页面加载和重新刷新
  • 页面切换路由
前后端trace数据(数据量较大)
  • 存储于
    localStorage.pageGuid.bucket中;
  • bucket中数量超过阈值,触发上报;
  • 页面卸载unload,触发上报;
2.
上报方式
如何上报性能数据,我们第一反应就是通过ajax请求的形式来上报前端性能数据。这种方法有一些缺陷,比如必须对跨域做特殊处理以及如果页面销毁后,相应的ajax方法并不一定发送成功等问题。
其中跨域的问题比较好处理,最难解决的问题是
如果页面销毁,那么对应的ajax方法并不一定能成功发送。
根据google analytics(GA)中的方法,根据浏览器的兼容性以及url的长度,来采用不同的方法上报性能数据:
通过动态创建img标签的方式,在img.src中拼接url的方式发送请求,不存在跨域限制。如果url太长,则才用sendBeacon的方式发送请求,如果sendBeacon方法不兼容,则发送ajax post同步请求。

(1)、sendBeacon方法

解决在文档卸载或者页面关闭后无法完成异步ajax请求的问题,很多情况下我们会把异步变成同步。在页面卸载的unload或者beforeunload事件中执行同步方法调用。
但是同步方法调用存在一个问题,就是会推迟A页面切换进入B页面的时间。而sendBeacon方法解决了该问题,简单来说:
sendBeacon方法在页面销毁期,可以异步的发送数据,因此不会造成类似同步ajax请求那样的阻塞问题,也不会影响下一个页面的渲染
sendBeacon的调用方式为:
function sendBeacon(url,data){//判断支不支持navigator.sendBeacon
  let headers ={
type:
'application/x-www-form-urlencoded'};
let blob
= newBlob([JSON.stringify(data)], headers);
navigator.sendBeacon(url,blob);
}

(2)动态创建img标签的形式

通过动态创建img标签的形式,指定src属性所指定的url来发送请求,首先不受跨域的限制,其次img标签动态插入,会延迟页面的卸载保证图片的插入,因此可以保证在页面的销毁期,请求可以发生。
function imgReport(url, data) {if (!url || !data) {return;
}
let image
= document.createElement('img');
let items
=[];
items
=JSON.Parse(data);
let name
= 'img_' + (+newDate());
image.onload
= image.onerror =function () {

};
let newUrl
= url + (url.indexOf('?') < 0 ? '?' : '&') + items.join('&');

image.src
=newUrl;
}

(3)同步ajax post请求

动态创建img标签的方法,拼接url的时候存在一定的问题,因为浏览器对url的长度是有限制的。而sendBeacon方法兼容性不是很好,最后兜底的处理方式就是发送同步的ajax请求,同步的ajax请求前面说过,会在页面销毁期之前执行,虽然会有一定程度的阻塞下一个页面的渲染。
function xmlLoadData(url,data) {
  var client = new XMLHttpRequest();
  client.open("POST", url,false);
  client.setRequestHeader("Content-Type", "application/json; charset=utf-8");
  client.send(JSON.stringify(data));
}

(4)综合解决方案

首先拼接携带参数的完整的url,判断url的长度,如果url的长度小于浏览器允许的最大长度内,那么通过动态创建img标签的形式来发送前端性能数据,如果url太长,则判断浏览器是否支持sendBeacon方法,如果支持,则通过sendBeacon方法来发送请求,否则发送同步的ajax请求。
function dealWithUrl(url,appId){
let times
=performanceInfo(appId);
let items
=decoupling(times);
let urlLength
= (url + (url.indexOf('?') < 0 ? '?' : '&') + items.join('&')).length;if(urlLength<2083){
imgReport(url,times);
}
else if(navigator.sendBeacon){
sendBeacon(url,times);
}
else{
xmlLoadData(url,times);
}
}

3、方案设计(Hybrid H5)

3.1总体架构

3.2.上报流程

3.3.流程时序图

4.服务端采集及分析

搭建OAP服务,进行数据清洗加工及展示

一.PyQt5简介

PyQt5是一个用于创建图形用户界面(GUI)应用程序的跨平台工具集,它将Qt库(广泛用于C++编程语言中创建丰富的GUI应用程序)的功能包装给Python使用者。PyQt5是由Riverbank Computing开发的,并且可以在所有主流操作系统上运行,包含Windows、macOS和Linux。

PyQt5包括了超过620个类和6000个函数和方法。这个框架支持包括SQL数据库、线程、Unicode、正则表达式、网络编程等高级功能。除了GUI功能外,PyQt5还允许用户访问Qt的模型/视图架构和QML(Qt Modeling Language),这是一种专门为创建动态和自定义用户界面而设计的语言。

二.环境搭建

1.终端使用pip安装PyQt5库

pip install -i https://pypi.tuna.tsinghua.edu.cn/simple PyQt5

2.如果出现Could not build wheels for PyQt5_sip, which is required to install pyproject.toml-based projects报错信息

3.先安装 Visual Studio 后再安装PyQt5即可,终端执行如下指令

wget https://aka.ms/vs/17/release/vs_BuildTools.exe -o vs_BuildTools.exe ; cmd /c vs_BuildTools.exe

勾选C++/CLI后安装就行

三.基本用法

1.PyQt5常见的模块

QApplication 这个类管理GUI应用程序的控制流和主要设置,并且是每个PyQt5应用程序中必须有的部分
QWidget 所有用户界面对象的基类。当你想创建一个自定义的窗口时,你会使用或者继承这个类
QLabel 用于展示文本或图片的类
QtCore 其他模块使用的核心非 GUI 类
QAction 用于处理菜单栏、工具栏或快捷键等的动作。
QtSql 使用 SQL 进行数据库集成的类
QtXml 处理 XML 的类
QSlider 滑动条,让用户通过滑动选择一个数值。

2.一个简单的窗口创建

importsysfrom PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets importQWidget, QLabel, QMessageBox, QApplicationdefwindows():#创建一个 QApplication 类的应用程序对象
    app =QApplication(sys.argv)#基于QWidget类声明窗口
    w =QWidget()#添加一个QLabel对象,并将标签添加“helloworld”文本。
    b =QLabel(w)
b.setText(
"Hello World!")#设置文本控件在窗口中的位置(x,y) b.move(50, 50)#设置label控件的长和宽 b.resize(100,20)#设置字体样式大小 font =QFont()
font.setFamily(
"Arial") #字体样式,中文英文都可(“楷体”) font.setPointSize(20) #字体大小 b.setFont(font)#通过 setGeometry() 方法定义窗口的大小和位置(x,y,w,h) w.setGeometry(100, 100, 500, 1000)#设置窗口标题 w.setWindowTitle("PyQt5") #显示窗口 w.show()#进入应用程序的主循环app.exec_()方法(窗口一直显示) sys.exit(app.exec_())if __name__ == '__main__':
windows()

备注写的很详细了,这里就不一一介绍每行代码的意思了。

3.窗口icon设置

importsysfrom PyQt5.QtGui import *
from PyQt5.QtWidgets import QWidget, QApplication  #导入尽量用具体类,不用*


classWindow(QWidget):def __init__(self, parent=None):
super().
__init__(parent)
self.initUI()
self.setGeometry(
100, 100, 500, 500)
self.setWindowTitle(
"PyQt5")#设置icon definitUI(self):
self.setWindowIcon(QIcon(
'./2.jpg')) #设置窗口左上角icon defmain():
app
=QApplication(sys.argv)
ex
=Window()
ex.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()

上述代码中,我们在initUI函数中,调用父类setWindowIcon()方法去设置窗口icon,在Window类中初始化时去调用initUI函数来实现设置icon。

4.QLabel控件

    defqlabel(self):
font
=QFont()
font.setFamily(
"Arial")
font.setPointSize(
16)
label
=QLabel(self)
label.setText(
"Hello World") #设置文本 label.setFont(font) #设置文本字体类型及大小 label.move(50, 20) #控件在窗口的位置

这里和如上写法一致,在Window类中写一个qlabel函数,里面去调用QLabel,再在Window类中初始化时调用这个函数即可(使用QLabel模块时需提前导入该模块),我们来看下结果。

5.tips信息提示框

    deftips(self):#创建一个按钮控件
        btn = QPushButton('Button', self)#设置文本字体及大小
        btn.setFont(QFont('SansSerif', 50))#设置tips
        btn.setToolTip('This is a widget')#设置按钮在窗口的位置
        btn.move(100, 100)

效果如下,鼠标悬浮在button上时,会出现tips信息弹框。

6.关闭二次确定弹框

主动关闭窗口(即点击窗口右上角X),弹出二次确定弹框,提示是否关闭。

importsysfrom PyQt5.QtGui import *
from PyQt5.QtWidgets import QWidget, QApplication, QMessageBox, QDesktopWidget  #导入尽量用具体类,不用*


classWindow(QWidget):def __init__(self, parent=None):
super().
__init__(parent)
self.initUI()
self.setGeometry(
100, 100, 500, 500)
self.setWindowTitle(
"PyQt5")
self.center()
#设置icon definitUI(self):
self.setWindowIcon(QIcon(
'./2.jpg'))def closeEvent(self, event): #关闭二次确定弹框 reply = QMessageBox.question(self, '是否关闭',"Are you sure to quit?", QMessageBox.Yes, QMessageBox.No)if reply ==QMessageBox.Yes:
event.accept() # 关闭窗口
else:
event.ignore() # 不关闭
def center(self): #设置窗口居中 qt = self.frameGeometry() #获取需要操作的窗口位置,长宽(即设置的setGeometry) cp = QDesktopWidget().availableGeometry().center() #获取电脑分辨率 qt.moveCenter(cp) #获取电脑中间位置 self.move(qt.topLeft()) #将窗口移动到中间位置 defmain():
app
=QApplication(sys.argv)
ex
=Window()
ex.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()

上述代码中重写了父类中的closeEvent方法,通过QMessageBox.question()来获取用户行为,event.accept()来确定关闭窗口。

7.关闭窗口事件

    defclos(self):
qbtn
= QPushButton('Quit', self) #创建一个按钮 qbtn.clicked.connect(QCoreApplication.instance().quit) #回调关闭事件 qbtn.setToolTip("点击关闭窗口") #tips提示 qbtn.move(0, 0)

如下图效果,鼠标点击Quit按钮后窗口关闭,其效果和关闭按钮X一致。

8.菜单栏和工具栏

QMainWindow
用于创建主应用程序窗口的类。它是
QWidget
的一个子类,提供了创建具有菜单栏、工具栏、状态栏等的主窗口所需的功能。

1)状态栏

importsysfrom PyQt5.QtWidgets importQMainWindow, QApplicationclassExample(QMainWindow):def __init__(self):
super().
__init__()
self.initUI()
definitUI(self):
self.setGeometry(
100, 100, 500, 500)
self.setWindowTitle(
'Statusbar')
self.statusBar().showMessage(
"状态栏") #设置状态栏 if __name__ == '__main__':
app
=QApplication(sys.argv)
ex
=Example()
ex.show()
sys.exit(app.exec_())

状态栏的设置很简单,通过调用QMainWindow的statusBar()方法创建状态栏,showMessage()来设置状态栏显示的文本。

2)菜单栏

    defbar(self):
action
= QAction(QIcon('./2.jpg'), '登录', self) #创建QAction实例 action.setShortcut('Ctrl+a') #设置快捷键操作 action.setStatusTip('登录') #状态栏提示,窗口的左下角出现,鼠标悬停在选项上会出现 action.triggered.connect(qApp.quit) #点击事件后回调的方法(qApp.quit关闭窗口) menubar= self.menuBar() #创建一个菜单栏 menu = menubar.addMenu('File') #设置菜单栏tab menu.addAction(action)#关联事件

用过menuBar()创建一个菜单栏,并添加addMenu()tab项,通过addAction()添加QAction行为动作,具体效果如下。

triggered.connect(qApp.quit)设置了触发动作后回调事件,即点击登录选项或快捷键Ctrl+A时会触发qApp.quit关闭窗口。上诉中我们只创建了一个菜单栏并只绑定了一个QAction如果想要多个菜单栏多个动作,可以创建多个addMenu(),下面看下具体实例。

   defbar(self):
action
= QAction(QIcon('./2.jpg'), '登录', self) #创建QAction实例 action.setShortcut('Ctrl+a') #设置快捷键操作 action.setStatusTip('登录') #状态栏提示,窗口的左下角出现,鼠标悬停在选项上会出现 action.triggered.connect(qApp.quit) #点击事件后回调的方法(qApp.quit关闭窗口) action1= QAction(QIcon('./2.jpg'), '登录', self) #创建QAction实例 action1.setShortcut('Ctrl+a') #设置快捷键操作 action1.setStatusTip('退出') #状态栏提示,窗口的左下角出现,鼠标悬停在选项上会出现 action1.triggered.connect(qApp.quit) #点击事件后回调的方法(qApp.quit关闭窗口) menubar= self.menuBar() #创建一个菜单栏 menu = menubar.addMenu('File') #设置菜单栏tab menubar1 = self.menuBar() #创建一个菜单栏 menu1 = menubar1.addMenu('File1') #设置菜单栏tab menu.addAction(action)#关联事件 menu.addAction(action1) #关联事件

3)子菜单栏

上面我们只是实现了一级菜单栏,那么在需求中要求创建二级,三级或者多级菜单,此时就需要用到QMenu()来创建下拉菜单栏了,我们先来看下二级菜单栏的创建。

    #创建子菜单
    defbar_z(self):
menubar
= self.menuBar() #创建菜单栏 menu = menubar.addMenu('File') #设置菜单栏tab newmenu= QMenu('一级下拉框', self) #创建一个下拉菜单 impAct = QAction('二级目录', self) #设置一个动作 impAct.triggered.connect(self.btn) #点击事件后回调的方法 newmenu.addAction(impAct) #添加动作到下菜单中 newact= QAction('一级目录', self) #创建动作 menu.addAction(newact)#添加动作到菜单栏 menu.addMenu(newmenu) #添加下拉菜单到菜单栏 defbtn(self):#创建一个按钮控件 btn = QPushButton('Button', self)#btn.setGeometry(100, 100, 80, 30) btn.move(100, 100)
btn.setToolTip(
"button")
btn.show()

如果我们想创建三级目录甚至更多级的目录,可以创建多个下拉菜单栏,并通过addMenu()添加进去,下面看下具体实现。

#创建子菜单
    defbar_z(self):
menubar
= self.menuBar() #创建菜单栏 menu = menubar.addMenu('File') #设置菜单栏tab newmenu1= QMenu('二级下拉框', self) #创建一个下拉菜单 impAct1 = QAction('三级目录', self) #设置一个动作 impAct1.triggered.connect(self.btn) #点击事件后回调的方法 newmenu1.addAction(impAct1) #添加动作到下菜单中 newmenu= QMenu('一级下拉框', self) #创建一个下拉菜单 impAct = QAction('二级目录', self) #设置一个动作 impAct.triggered.connect(self.btn) #点击事件后回调的方法 newmenu.addAction(impAct) #添加动作到下菜单中 newmenu.addMenu(newmenu1) #添加下拉菜单栏到二级菜单下 newact= QAction('一级目录', self) #创建动作 menu.addAction(newact)#添加动作到菜单栏 menu.addMenu(newmenu) #添加下拉菜单到菜单栏

4)带复选框的菜单

    defbar_checkable(self):
menubar
=self.menuBar()
bar_menu
= menubar.addMenu('View') #添加菜单栏 statact = QAction('复选框', self, checkable=True) #创建一个可以勾选的动作 statact.setStatusTip('View statusbar')
statact.setChecked(True)
#设置默认选中 statact.triggered.connect(self.menu)
bar_menu.addAction(statact)
defmenu(self, state):ifstate:print("勾选后执行的事件") #勾选了 else:print("取消勾选执行的事件") #取消勾选

QAction()中,checkable=True表示可以勾选的动作。

5)右键栏

鼠标悬浮再窗口上,右击打开的菜单栏,此功能需要重写父类中contextMenuEvent()方法,看下代码演示。

    defcontextMenuEvent(self, event):
rightmenu
= QMenu(self) #创建一个下拉菜单 #添加动作 d = rightmenu.addAction("打印")
q
= rightmenu.addAction("退出")
ture
= rightmenu.addAction("确定")
action
= rightmenu.exec_(self.mapToGlobal(event.pos())) #exec_()显示菜单。获取动作行为 if action ==d:
self.btn()
elif action ==q:
qApp.quit()
#关闭窗口 else:print("无确定内容")

6)工具栏

工具栏可以理解为是多个动作的集合,可以将多个QAction()添加展示出来。

    def bar_tool(self):  #多个动作集合
        action = QAction("工具1",self)
action2
= QAction("工具2",self)
action.setStatusTip(
"点击退出")
action.triggered.connect(qApp.quit)
toolbat
= self.addToolBar("工具栏") #创建一个工具栏 toolbat.addAction(action) #将动作添加到工具栏 toolbat.addAction(action2) #将动作添加到工具栏

7)主窗口显示

这个就不做过多介绍,在初始__init__中调用上诉几种方法即可,默认菜单栏在最顶部,添加的动作会更加调用的先后从左到右排列,工具栏默认在菜单栏下发,这里只看下效果截图。

9.页面布局

1)坐标布局


importsysfrom PyQt5.QtGui importQIconfrom PyQt5.QtWidgets importQLabel, QApplication, QWidgetclassExample(QWidget):def __init__(self):
super().
__init__()
self.initUI()
definitUI(self):
self.setGeometry(
100, 100, 500, 500)
self.setWindowTitle(
'Statusbar')
q1
= QLabel("quit", self)
q2
= QLabel("quit2", self)
q3
= QLabel("quit3", self)
q1.move(
20, 20) #坐标布局 q2.move(20, 80) #坐标布局 q3.move(20, 140) #坐标布局 if __name__ == '__main__':
app
=QApplication(sys.argv)
ex
=Example()
ex.show()
sys.exit(app.exec_())

坐标布局

上述中通过move(x, y)方法来设置qlable的位置

2)界面比例布局


importsysfrom PyQt5.QtGui importQIconfrom PyQt5.QtWidgets importQApplication, QPushButton, QHBoxLayout, QWidgetclassExample(QWidget):def __init__(self):
super().
__init__()
self.initUI()
self.button()
definitUI(self):
self.setGeometry(
100, 100, 500, 500)
self.setWindowTitle(
'Statusbar')def button(self): #比例布局 ok = QPushButton("ok") #创建三个按钮 cancel = QPushButton("cancel")
cancel1
= QPushButton("cancel1")

hbox1
= QHBoxLayout() #创建一个水平布局 hbox1.addWidget(ok) #添加按钮到水平布局中 hbox1.addStretch(1) #设置水平比例间距 hbox1.addWidget(cancel)#添加按钮到水平布局中 hbox1.addStretch(1) #设置水平比例间距 hbox1.addWidget(cancel1)
hbox1.addStretch(
6)

self.setLayout(hbox1)
#添加到布局器 if __name__ == '__main__':
app
=QApplication(sys.argv)
ex
=Example()
ex.show()
sys.exit(app.exec_())

水平布局

上面代码中通过QHBoxLayout() 创建一个水平布局,addWidget()将按钮添加到布局中,再通过addStretch()来设置比例间距,可以看到设置的比例为1:1:6


importsysfrom PyQt5.QtGui importQIconfrom PyQt5.QtWidgets importQApplication, QPushButton, QHBoxLayout, QVBoxLayout, QWidgetclassExample(QWidget):def __init__(self):
super().
__init__()
self.initUI()
self.button()
definitUI(self):
self.setGeometry(
100, 100, 500, 500)
self.setWindowTitle(
'Statusbar')def button(self): #比例布局 ok = QPushButton("ok") #创建三个按钮 cancel = QPushButton("cancel")
cancel1
= QPushButton("cancel1")

hbox
= QHBoxLayout() #创建一个水平布局 hbox.addStretch(1) #设置水平比例间距(只设置一个stretch,会将按钮挤到最右侧。若stretch写在addWidget下面,则按钮会被寄到最左侧) hbox.addWidget(ok) #添加按钮到水平布局中 hbox.addWidget(cancel)

vbox
= QVBoxLayout() #创建一个垂直布局 vbox.addStretch(1) #设置垂直比例间距(只设置一个stretch,会将按钮挤到最下面。若stretch写在addlayout下面,则按钮会被寄到最下面) vbox.addLayout(hbox) #将刚刚创建的水平布局添加到垂直布局中 self.setLayout(vbox)#将垂直布局加到布局器中(按钮位于页面右下角) if __name__ == '__main__':
app
=QApplication(sys.argv)
ex
=Example()
ex.show()
sys.exit(app.exec_())

垂直布局

3)栅格化布局


importsysfrom PyQt5.QtGui importQIconfrom PyQt5.QtWidgets importQApplication, QPushButton, QWidget, QGridLayoutclassExample(QWidget):def __init__(self):
super().
__init__()
self.initUI()
self.Grid()
definitUI(self):
self.setGeometry(
100, 100, 500, 500)
self.setWindowTitle(
'Statusbar')def Grid(self): #栅格化的按钮 grid = QGridLayout() #创建一个栅格化布局 name = ["7", "8", "9", "/","4", "5", "6", "x","1", "2", "3", "-","清除", "0", ".", "="]#列表推导式 pos = [(x, y) for x in range(4) for y in range(4)]for names, p in zip(name, pos): #同时迭代两个序列 button = QPushButton(names) #创建按钮 grid.setSpacing(10) #设置各个单元格之间的间距 grid.addWidget(button, *p) #添加到栅格化布局中 self.setLayout(grid) #将栅格化布局加到布局器 if __name__ == '__main__':
app
=QApplication(sys.argv)
ex
=Example()
ex.show()
sys.exit(app.exec_())

栅格化布局

效果图

栅格化布局理论上也是通过坐标来定位的,通过列表推导式可到得到几个坐标(0,0)(0,1)(0,2)(1,0)(1,1)(1,2)..........,然后再通过坐标去逐个添加。

4)实例


importsysfrom PyQt5.QtGui importQIconfrom PyQt5.QtWidgets importQLabel, QApplication,QWidget, QGridLayout, \
QLineEdit, QTextEdit
classExample(QWidget):def __init__(self):
super().
__init__()
self.initUI()
self.input()
definitUI(self):
self.setGeometry(
100, 100, 500, 500)
self.setWindowTitle(
'Statusbar')definput(self):
grid
=QGridLayout()#设置标签 title = QLabel("title")
Author
= QLabel("Author")
Review
= QLabel("Review")#设置输入框 titleEdit = QLineEdit() #行编辑 authorEdit =QLineEdit()
reviewEdit
= QTextEdit() #文版编辑 grid.setSpacing(10) #设置间距 grid.addWidget(title, 0, 0)
grid.addWidget(titleEdit, 0,
1)
grid.addWidget(Author,
1, 0)
grid.addWidget(authorEdit,
1, 1)
grid.addWidget(Review,
2, 0)
grid.addWidget(reviewEdit,
2, 1)

self.setLayout(grid)
if __name__ == '__main__':
app
=QApplication(sys.argv)
ex
=Example()
ex.show()
sys.exit(app.exec_())

实例

上述中实例通过QGridLayout()来将QLabel(), QLineEdit(行编辑)  ,QTextEdit(文版编辑) 进行的整合使用,下面看下结果。

四.实例

以一个简单计算器功能为例,先来看下代码实现和效果展示


importsysfrom PyQt5.QtGui importQIconfrom PyQt5.QtWidgets importQApplication, QPushButton, QWidget, QGridLayout, \
QLineEdit
classExample(QWidget):def __init__(self):
super().
__init__()
self.initUI()
self.Grid()
definitUI(self):
self.setGeometry(
100, 100, 500, 500)
self.setWindowTitle(
'Statusbar')def Grid(self): #栅格化的按钮 grid =QGridLayout()
hbox
=QLineEdit()
grid.addWidget(hbox, 0, 0,
1, 4)
name
=["7", "8", "9", "/","4", "5", "6", "*","1", "2", "3", "-","清除", "0", ".", "="]#列表推导式 pos = [(x, y) for x in range(1,5) for y in range(4)]for names, p in zip(name, pos): #同时迭代两个序列 if names == "":continue elif names == "hbox":
grid.addWidget(hbox,
*p)else:
button
=QPushButton(names)
grid.addWidget(button,
*p)
button.clicked.connect(
lambda checked, btn_text=names: on_button_clicked(btn_text))
grid.setSpacing(
10) #设置各个单元格之间的间距 self.setLayout(grid)defon_button_clicked(btn_text):try:if btn_text == "清除":
hbox.clear()
elif btn_text == "=":
str_num
=eval(hbox.text())
hbox.clear()
hbox.insert(str(str_num))
else:
hbox.insert(btn_text)
except:
hbox.insert(
"error")if __name__ == '__main__':
app
=QApplication(sys.argv)
ex
=Example()
ex.show()
sys.exit(app.exec_())

计算器

使用addWidget将输入框添加到栅格化布局中,后面无个参数分别表示:(添加对象,x位置,y位置,占据的单元格高度,占据的单元格宽度)。

grid.addWidget(hbox, 0, 0, 1, 4)

QLineEdit()几个常用的方法

        hbox =QLineEdit()
hbox.setText(
"默认文案") #设置默认文案 hbox.setPlaceholderText("暗文") #当输入框内容为空时显示该文案 hbox.insert("243") #添加 print(hbox.text()) #获取框内容 hbox.clear() #清空

五.打包

可参考:https://www.cnblogs.com/lihongtaoya/p/17349911.html

文章来源:https://www.cnblogs.com/lihongtaoya/ ,请勿转载

今年的中国系统架构师大会(SACC)在我所在的城市广州举办,很荣幸受邀参加。这次能接触到国内最优秀的架构师,学习他们的架构思想和行业经验。对我而言非常有意义。

在这里插入图片描述

大会分为上下午共4场,我参加了上午的多云多活架构设计专场和下午的AIGC专场。

本篇文章就多云多活架构设计专场,我选取几位老师的观点进行分享。(我并不是架构师,只是对架构感兴趣,如有错误,还请指正)

张晓辉 Flomesh 高级云原生架构师

在这里插入图片描述

张老师分享了混合多云架构的技术方案,这里可以体现一个企业在技术层面发展到什么样的程度,根据这张图可以看看企业架构的复杂程度。可能大部分企业还在容器化,分布式架构和单一集群的复杂程度。

在这里插入图片描述

在资源抽象层面,利用统一的K8s Api,可以实现不同云厂商的资源抽象,实现资源的复用。K8s多集群可以作为防腐层,屏蔽基础设施层差异,避免厂商锁定

在这里插入图片描述

李中原 (平安银行股份有限公司 国产数据库技术负责人 )

在这里插入图片描述

银行系统的技术架构需要满足高吞吐、低延迟要求,业务可靠性的要求,以及数据安全性的要求,同时,基于监管和法规的要求,技术架构需要自主可控。

李老师分享了银行架构的“去IOE化”的变迁。

在这里插入图片描述

对于可靠性要求,从传统的不能容许服务中断,逐渐过渡到允许服务中断但必须迅速恢复。

黄奕青 (腾讯云 技术专家)

黄老师对两位讲师做了补充,着重对异地多活架构中的问题,以及解决方案和设计要点等“三阶六要点”进行了分享。

  1. 技术架构的三个阶段
  • 第一阶段是对业务需求的解读:一个是传统的需求分析,一个是企业聘请领域专家对需求建模
  • 第二阶段是确立架构,目前较为合理的架构是标准的微服务架构,已经被广泛应用(两地三中心架构)。
  • 第三阶段是在微服务架构之上构建单元化,该阶段需要解决如下异地多活架构中的挑战。
  1. 异地多活架构的挑战
  • 因为网络基础设施的物理局限性,需要考虑异地资源使用时的延迟问题。
  • 数据访问效率问题:跨地域的数据访问速度慢会导致核心交易延迟,需要考虑数据切分和流量闭环治理。
  • 分布式事务成本问题:跨地域的分布式事务会带来高成本和复杂性。
  • 中间件的单元化考虑:在异地架构中,需要考虑中间件的单元化和调度。
  1. 六大设计要点
  • 数据切分:设计大型分布式系统时首先要考虑数据如何切分。 数据按什么维度切分最合适,(如按客户号,还是哈希,按range范围?)
  • 流量闭环与单元化架构:黄老师强调了流量闭环的重要性,特别是在单元化架构中。这意味着流量需要在特定的单元内部循环,而不是跨单元流动,以减少无效的横向流量。为了实现这一目标,网关需要具有状态感知能力,能够识别客户信息并将流量路由到正确的单元。此外,单元和上层计算资源之间的逻辑绑定也是关键。
  • 中间件单元化:在单元化架构中,中间件(如MQ、Redis等)的链路也需要考虑单元化。特别是在跨单元的场景下,如客户之间的转账,会涉及到分布式事务的问题。这种分布式事务的调度和管理在异地多活架构下会被放大,成为实现的难点。
  • 数据切分与聚合查询:数据被切分到多个库后,聚合查询变得困难。黄老师提到了从前在一个库下可以轻松进行多表的聚合查询,但现在需要访问多个库进行查询。这需要对现有的查询策略和技术进行重新的设计和优化。
  • 业务连续性保障:单元之间的互备和业务连续性保障是必要的。
  • 分布式运维:针对大型核心系统的分布式运维需要特别关注。

戴骏贤 (网易游戏资深数据库系统工程师 )

网易游戏对于RPO的要求相对苛刻(数据要求无损)。但更多是以成本最小化为考量实现双活高可用架构。因此目前仅采用同城双副本。

李运华 互联网大厂 资深技术专家

企业是否需要上云取决于成本和技术实力两个主要因素。

  1. 成本考量
  • 企业规模
    :一般来说,如果企业的服务器规模在一千台以下,上云通常是成本效益较高的选择。对于规模在一千到五千台之间的企业,上云可能并不一定省成本,而取决于其在云平台上的使用程度。然而,如果服务器规模超过五千台,可能会发现下云(即从云平台迁移到自建环境)会更经济合算,尤其是对于大型企业。这是因为大型企业可能会拥有足够的资源和资金来自建云或自建机房,并且会节省大量成本。
  • 全面成本考量
    :除了物理服务器成本之外,还需要考虑人员成本、维护成本等方面的费用。这些费用在决定是否上云时也至关重要。
  1. 技术实力
  • 企业规模与技术实力
    :大型企业通常拥有更多的人力资源和技术实力,因此更有能力独立管理和运维自己的服务器环境。相比之下,中小型企业可能缺乏专业技能和资源,难以应对复杂的技术挑战,因此更倾向于使用云服务提供商提供的解决方案。
  • 云产品的功能和强大程度
    :云服务提供商提供了丰富的云产品和服务,包括分布式一致性数据库等高级功能。然而,自行实现这些功能需要大量的时间、资源和技术实力。对于中小型企业而言,使用云平台提供的功能可能更为经济实惠和可行。(ocean base一个几百上千人的团队,从2013年开始,搞了12年)

同城双活和异地多活有哪些方案?

  1. 同城双活方案

    由于同城机房之间的网络延迟可以做到非常低,通常可以在几毫秒以内,因此同城双核在逻辑上可以看作是一个统一的机房。可以直接按照集群的方式运作,成本和复杂度会低。

  2. 近邻的异地多活

    这种方案通常是指在相邻的两个城市之间进行异地多活部署。例如,广州与深圳、杭州与上海等相邻城市之间的部署。由于网络延迟相对较低,大约在十毫秒左右,因此可以接受。近邻的异地多活方案通常可以通过分布式一致性算法来实现投票和选举,确保系统的可用性。

  3. 远端的异地多活

    在距离较远的城市之间进行异地多活部署,如广州与北京之间的部署。网络延迟可能较高(30ms以上),本质上没有办法实现这种集群的运作。

马洪喜(深圳行云创新科技有限公司 CEO)

马老师分享了三个不同企业的案例:

  1. 区域性银行的高可用解决方案
  • 该银行实施了同城双活的云原生高可用解决方案,这是一个强需求,因为银行业务对高可用性有极高的要求。
  • 他们的方案是基于一个调度器实现的,使得业务在发生故障时可以切换到单机群模式。
  1. 某超大型电器公司的混合云方案
  • 该公司实施了混合云,将国内业务部署在私有云上,将海外业务部署在AWS上。
  • 这是出于业务需求的考虑,例如海外业务需要在AWS上就近访问。
  1. 某锂电制造企业的工业场景需求
  • 该企业有着复杂的IT/OT融合场景,需要在制造业中实现微服务和AI的应用。
  • 他们的需求包括在本地数据中心或厂区内的小型数据中心处理数据,而不是将数据从生产线传送回总部或云平台。
  • 制造业趋向于建立一个多级算力体系,包括本地数据中心、云平台和边缘计算,而不仅仅是简单的双活或多活部署。

另外在与公有云对接的过程中,确实会遇到一些挑战和坑:比如公有云平台通常会限制用户在容器服务中运行某些特定类型的应用程序或容器镜像。以及路由设置、网络安全限制等。此外,有些云服务商在网络层面可能会实施一些特殊的技术手段,如MAC地址劫持等,导致一些不必要的麻烦和延迟。

顺炽国 (某制造集团 云平台基础服务负责人 )

常见的一些互联网应用很多只关注C端的,也就是只需要考虑北线(用户入口)入口流量一个分流跟分发或者流量管控。但是在物联网行业还需要考虑南线(设备入网)的流量管理,这两部分只有配网绑定的阶段需要打通南北两线的数据。

  1. 成本压力
    :集团的服务器规模正好在一千到五千个主机之间,导致了巨大的成本和运维压力。解决这个问题正在考虑下云。

  2. 业务复杂性
    :智能家电行业有独特的业务特点,需要同时关注北线(用户入口)和南线(设备入网)的流量管理,确保设备与用户之间的关联和通信畅通。

  3. 多云节点部署
    :需要根据业务需求,在各个区域部署多云节点,以提供更快的服务响应时间,降低用户体验的糟糕程度。

  4. 单元化思路
    :采用单元化的思路,尽量减少数据同步带来的问题,通过北线和南线的网关,将业务闭环在单元内,以降低对专线的重度依赖,提高业务稳定性。

张观石( 泰健科技 CTO)

张老师讲了个故事: 初始阶段,自建CDN成本高,后接入云CDN,但谈判降价困难。后期通过多云CDN架构,平衡质量和成本,使各厂商主动降价争取份额。

他从一个比较新颖的角度谈论了多云架构的优势:根据提供商的服务质量和价格,分配负载份额

另外他谈到了多云架构的优势:

  • 多云并非只为实现多活,而是为了在架构中灵活利用不同云服务的特点和价格优势(使用特定GPU型号的业务只能在相应云上部署,提高架构灵活性)
  • 利用不同云的产品优势,如低价存储,以降低整体成本。

通过多云策略,企业可以增加与云服务商的谈判地位,从而更好地争取资源、降低成本。同时,多云策略也提供了更灵活的资源供给方式,可以根据业务需求在不同云之间调度资源,确保服务的稳定性和容灾能力。

另外由于多云架构的复杂性,监控告警、排障流程等运维问题复杂度增加,需要考虑跨云沟通和统一运维平台的建设。这就引入了另一个问题:如何建立多云融合统一管理(CMDB)

CMDB的核心功能是收集和存储关于云环境中各种资源的详细信息,包括虚拟机、存储、网络设备等。管理员可以实现统一的交互界面,方便地查询、分析和修改资源的配置状态

根据企业对于多云管理的管理要求,可以采用以下几种方式:

  • 十几二十几台服务器,没有必要上CMDB系统,通过管理员登录各云的控制台进行管理

  • 资源管理型:创建虚拟机,配置网络等,基于开源的管理工具(比如rental?)二次开发较为容易,各家云厂商的API接口并不复杂。

  • 应用管理型,属于高级别应用,可以与应用交付系统结合,从应用的视角出发,自动选择合适的云资源来部署和交付应用,基本要采购商业产品。定制化开发。或寻求混合云厂商,有现有的解决方案可选。

--完结--

原文:
Android 圆形进度条ProgressBar实现固定进度-Stars-One的杂货小窝

之前遇到一个问题,发现Android里的圆形进度条无法固定一个进度,记录一下解决方法

探究

假设我们在xml中这样写:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".page.home.HomeFragment">

    <ProgressBar
        android:indeterminate="false"
        android:id="@+id/pbStorage"
        android:progress="60"
        android:max="100"
        android:layout_width="100dp"
        android:layout_height="100dp"/>

</FrameLayout>

按照预料的,上面的我们的进度条应该是圆形,且设置indeterminate属性,应该是固定了进度,但实际效果如下:

进度条是一直在转动的

之后通过翻阅官方的文档发现,ProgressBar虽然提供了一个圆形和水平进度条的样式,但是圆形的样式它不支持确定进度

我们可以打开其内置的theme来找到答案,如下的2个截图

解决

那么我们应该如何解决?很简单,我们可以从参考上面的水平进度条,设置对应的drawable属性即可解决

  • progressDrawable
    :用于设置
    ProgressBar
    的进度条样式。可以通过指定一个drawable资源来定义进度条的外观。通常用于显示确定的进度,即进度会从0%到100%之间变化。
  • indeterminateDrawable
    :用于设置
    ProgressBar
    在不确定进度时的样式。当
    ProgressBar
    处于不确定进度(indeterminate)时,进度条会显示一个循环动画,而不是固定的进度。通过指定一个drawable资源,可以定义不确定进度时的样式。

当我们设置Progressbar的
indeterminate
属性的时候为true,则ProgressBar使用
indeterminateDrawable
这个外观,否则则使用
progressDrawable
这个外观

于是我们可以自定义一个圆形的外观drawable,名为
progress_bar_green.xml
(放在drawable资源目录下),代码如下:

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:id="@android:id/background">
        <shape
            android:innerRadiusRatio="3.5"
            android:shape="ring"
            android:useLevel="false"
            android:type="sweep"
            android:thicknessRatio="12.0">
            <!--    进度条默认底色        -->
            <solid android:color="#f2f2f2"/>
        </shape>
    </item>

    <item android:id="@android:id/progress">
        <rotate
            android:pivotX="50%"
            android:pivotY="50%"
            android:fromDegrees="-90"
            android:toDegrees="-90">
            <shape
                android:innerRadiusRatio="3.5"
                android:shape="ring"
                android:angle="0"
                android:type="sweep"
                android:thicknessRatio="12.0">
                <!--    进度条颜色            -->
                <solid android:color="#33cf59"/>
            </shape>
        </rotate>
    </item>
</layer-list>

PS: 如果想要进度条的圆环宽度变小,可以将
thicknessRatio
属性调大,如果2个thicknessRatio的数值不一致,则是出现下面这种效果,看需求自行调整

之后xml布局里使用此drawable:

<ProgressBar
+        style="@style/Widget.AppCompat.ProgressBar.Horizontal"
+        android:progressDrawable="@drawable/progress_bar_green"
+        android:indeterminate="false"
        android:id="@+id/pbStorage"
        android:progress="60"
        android:max="100"
        android:layout_width="100dp"
        android:layout_height="100dp"/>

这里发现必须要使用
Widget.AppCompat.ProgressBar.Horizontal
这个样式才能使我们的progressDrawable属性生效,
猜测是ProgressBar内部或者Android系统内部应该是固定ProgressBar那个圆形默认样式为不确定(且不允许设置为确定进度)


具体原因就不深究了,有兴趣的读者可以研究研究,在评论区回复告知我

简介

Rust中最知名的两个web框架要数
Rocket

Actix
了,Rocket更注重易用性,Actix则更注重性能。这里只是了解一下Rust下的WebAPI开发流程,就学一下最简单的 Rocket。

Rocket 是一个用于 Rust 的异步 Web 框架,专注于可用性、安全性、可扩展性和速度:
github:
https://github.com/rwf2/Rocket/tree/v0.5
官网:
https://rocket.rs

hello world

需要最新版本的 Rust 来运行 Rocket 应用程序,运行以下命令确保安装了最新的工具链:

rustup default stable

创建一个新的基于二进制的 Cargo 项目并切换到新目录:

cargo new hello-rocket --bin
cd hello-rocket

执行以下命令,添加 Rocket 依赖项:

cargo add rocket

在 src/main.rs 文件中添加以下代码:

#[macro_use] extern crate rocket;

#[get("/")]
fn index() -> &'static str {
    "Hello, world!"
}

#[launch]
fn rocket() -> _ {
    rocket::build().mount("/", routes![index])
}

上面hello world示例没有main函数,main函数由
launch
宏生成,可以通过源码看出:

pub fn launch(args: TokenStream, input: TokenStream) -> TokenStream {
    emit!(attribute::entry::launch_attribute(args, input))
}
//...
async_entry!(launch_attribute, launch::Launch, quote!(fn main() {}));

运行程序,访问
http://localhost:8000
以查看应用,VS终端输出如下:
image

程序带的有彩色输出,如果在文件夹手动打开后没有彩色输出,说明系统不支持ANSI转义序列。

常用功能

动态路径

动态路径比较常见的场景是动态id场景,可以传N个动态类型即动态路径有多层,只要这个类型实现了
FromParam

//访问链接示例:http://localhost:8000/hello/张三/25/true
#[get("/hello/<name>/<age>/<is_male>")]
fn hello(name: &str, age: u8, is_male: bool) -> String {
    if is_male {
        format!("姓名 {} ,年龄 {}, 性别 男!", name, age)
    } else {
        format!("姓名 {} ,年龄 {}, 性别 女!", name, age)
    }
}

这个路由会匹配所有/hello/为基础路径的路由,然后将它匹配到的动态路径作为参数传递给处理器,Rocket默认给标准库里的一些常见类型以及Rocket自身的一些特殊类型实现了FromParam trait。

多个片段(segments)

可以通过<param..>的方式来匹配多个动态路径,这种类型的参数一般被叫做分段防护装置(segments guards),都必须先实现
FromSegments
这个trait。

use std::path::PathBuf;

//访问链接示例:http://localhost:8000/page/foo/bar
#[get("/page/<path..>")]
fn get_page(path: PathBuf) -> String {
    let mut output = String::new();
    for part in path.iter() {
        let part_str = part.to_string_lossy();
        println!("路径参数: {}", part_str);
        output.push_str(&format!("路径参数: {}\n", part_str));
    }
    output
}

PathBuf实现了FromSegments这个trait,所以不用担心/page或者/page//导致的解析失败,也不用担心路径遍历攻击(path traversal attacks)。

静态文件服务器

基于
分段防护装置(segments guards)
,可以简单的实现一个安全的静态文件服务器:

use std::path::{Path, PathBuf};
use rocket::fs::NamedFile;

#[get("public/<file..>")]
async fn files(file: PathBuf) -> Option<NamedFile> {
    NamedFile::open(Path::new("static/").join(file)).await.ok()
}

也可以使用
FileServer
,只需一行代码即可:

//引入FileServer结构体
use rocket::fs::FileServer;

//将/public作为URI前缀,并将static/作为文件路径
rocket.mount("/public", FileServer::from("static/"))

在项目根目录下创建一个名为static的文件夹,并将静态文件 example.txt 放在其中,通过以下uri访问文件:

http://localhost:8000/public/example.txt

在发布项目时,可以将静态文件夹放在与可执行文件相同的目录中,或者根据部署需求将其放在其他位置。

简单WebAPI示例

下面使用Rocket实现一个简单的WebAPI,这里的示例只实现Post方法,不涉及JWT鉴权。

添加依赖

执行以下命令添加
serde
依赖:

cargo add serde --features "derive"

再运行一遍以下命令,打开 json 功能标志:

cargo add rocket --features "json"

实现接口

在 src/main.rs 文件中实现以下代码:

#[macro_use] extern crate rocket;
use rocket::serde::{Deserialize, Serialize,json::Json};

#[derive(Debug, Deserialize)]
#[serde(crate = "rocket::serde")]
struct TaskRequest {
    description: String,
    complete: bool
}

#[derive(Debug, Serialize)]
#[serde(crate = "rocket::serde")]
struct TaskResponse {
    description: String,
    complete: bool
}

#[post("/todo", data = "<task>")]
fn my_function(task: Json<TaskRequest>) -> Json<TaskResponse> {
    // 处理接收到的任务
    println!("Received task: {:?}", task);

    // 返回处理后的任务
    Json(TaskResponse {
        description: task.description.clone(),
        complete: task.complete,
    })
}

#[launch]
fn rocket() -> _ {
    rocket::build().mount("/", routes![my_function])
}

接口测试

使用
curl
测试一下接口,在cmd中执行以下命令:

curl -X POST -H "Content-Type: application/json" -d "{\"description\":\"Task 1\",\"complete\":true}" http://localhost:8000/todo

测试结果:
image

参考链接