开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

以太网口硬件知识分享 一、了解网口通信基本原理 二、了解MD

发表于 2024-04-26

一、了解网口通信基本原理

实现网络通信实质上是PHY与MAC及RJ45接口实现信号传输。MAC 就是以太网控制器,MAC属于数据链路层,主要负责把数据封装成帧,对帧进行界定实现帧同步。对MAC地址和源MAC地址及逆行相应的处理并对错误帧进行处理。PHY属于物理层,在以太网控制器中负责物理层功能的芯片叫PHY芯片,因为网线上传输的是模拟信号而MAC发出或接收的信号为数字信号所以PHY主要负责对网络数据的编解码处理以及一些网络状态的控制。RJ45就是我们常用的网口座子。

在嵌入式领域通常MAC是被集成在CPU里面的,这种方案是目前的主流方案,PHY层(物理层)一般是使用专用的PHY芯片。如果要PHY芯片在一个特定的模式下工作就需要对PHY芯片进行控制,在这里用到的是MDIO总线。通过MDIO总线对PHY芯片的寄存器进行相应配置实现一些模式以及功能的控制。而数据信息,是由另外的网络协议接口进行传输,例如ELF 1上的PHY和MAC之间是用的RMII接口实现的数据传输,整体架构如图2.4所示:

二、了解MDIO总线

MDC是开漏(OD)输出,只能输出低电平,因此需要上拉处理,为MDIO提供时钟信号;根据KSZ8081RNB芯片手册(数据手册中查找),MDIO管理接口数据传输格式及含义如下表所示:

需要注意的是,TA在读操作和写操作两种状态下数据位不同,TA是介于寄存器地址和寄存器数据之间的2个bit位,用来转换数据传输方向。读操作时,地址传输和数据传输控制方不同,设置2bit TA的目的就是为了防止MDIO总线上产生竞争。TA的第1位z,PHY和MAC均释放总线控制输出高阻,且后面MAC一直保持高阻态状态,第2位0由PHY提供。第2位相当于一个应答信号,如果第2位为高电平,PHY无应答。除此之外,Idle为空闲状态,此时MDIO无源驱动,处高阻状态,但一般用上拉电阻使其处在高电平。

三、关于网络信号模式

常见网络信号模式包含:MII、RMII、GMII、RGMII、SGMII。每种模式包含的信号线数量和控制线数量各不相同,通讯速度也不相同:MII、RMII为百兆网络,GMII、RGMII、SGMII为千兆网络。区别和关系可以参考表2.2和图2.5。各模式工作原理可根据我们的视频进行详细了解。

常见网络模式区别

网络变压器

ELF 1使用的RJ45内部集成了网络变压器,其4、5脚位中心抽头引脚,该引脚有两种接法:电流型,中心抽头直接接电源;电压型,中心抽头通过100nF电容接地(ELF 1的接法,引脚4、5接100nF电容C87和C88)。

实际使用中需要如何处理中心抽头需要根据PHY芯片的驱动类型确定。

四、PHY芯片电路原理说明

ELF 1开发板最多支持两路百兆网口,底板(图2.5)和扩展板(图2.6)上各一路。ELF 1使用两个型号为KSZ8081RNB的PHY芯片实现网络通信,通过对前几段内容的了解可以分析出,KSZ8081RNB是通过MDIO总线挂载到ELF 1上的,MDIO总线对应芯片的11、12引脚主要负责通过配置PHY芯片KSZ8081RNB的寄存器控制PHY芯片的网口速率、网口双工模式、自协商使能等功能。KSZ8081RNB的数据接口是通过RMII和ELF 1连接,并传输网络数据的。

ENET_PHYAD0、ENET_PHYAD1和ENET_RXD1在芯片上电复位时会锁存一个电平状态作为芯片的PHY地址,在芯片上电后,其数据被锁存到相应寄存器中,而后恢复芯片的默认功能,不会影响通信。由于该款芯片有三条地址线,因此理论上同一块板子最多可以挂载7颗(001—111。000作为广播地址,是不可以被使用的);

ENET_CRS_DV、ENET_CFG1、ENET_CFG0是芯片接口模式选择, ELF 1设置为100;

底板网口原理图

扩展板网口原理图

网口电路设计指南

(1)ELF 1可支持两路百兆网。

(2)设计网口电路时8081的10号引脚必须连接6.49K_%1精度的电阻,否则可能会影响网口正常工作出现芯片无法挂载、网口不Link等问题。

(3)MDIO以及RMII接口的引脚要注意电平匹配,核心板为3.3V电平。

(4)MDIO总线上需要加上拉电阻阻值可以根据实际情况调整。

网口PCB设计指南

(1)MDIO总线上挂载多个PHY芯片时,使用串联方式,不要分叉布线

(2)RGMII接口分为发送信号,接收信号和控制信号,各组阻抗控制在50Ω±10%

(3)发送信号和接收信号,布线长度不超过100mm,组内信号长度误差不超过2.54mm

(4)时钟预留对地电容,方便后期调试

(5)MDI接口采用差分布线,阻抗100Ω±10%

(6)MDI组内差分误差不超过0.12mm

(7)芯片内部DCDC连接的功率电感要靠近芯片保证回路最短,并且保证地回路的完整;

(8)数据线上预留的串联电阻需要靠近源端放置;

(9)保护器件建议放置在变压器内侧,在变压器和PHY之间,靠近变压器;

(10)供电部分要考虑电流的大小,线宽尽量宽一点。要有足够的载流能力,滤波电容的位置尽量靠近芯片。

(11)网口 RJ45 在布线时要注意进行隔离地。

网口问题排查思路

在遇到网口问题时排查网口问题首先要明确问题点,网口不通的情况下首先要看 PHY 有没有成功挂载上,可通过是否可以启动网卡来判断,如果根本看不到设备节点或者在输入启动网卡的命令后报错,找不到 PHY 芯片说明 PHY 芯片没有成功挂载。如果可以正常启动网卡说明 PHY 可以成功挂载。

如果 PHY 没挂载上排查思路如下:

(1)首先看是否有缺件、少件,原件焊接错误的情况,PHY 芯片焊接是否过关等。

(2)检查各路供电是否正常,电源是否稳定,是否有明显压降、跌坑现象。滤波电容容量是否达标。

(3)检查复位电路是否有复位动作,复位后是否可以正常抬起复位信号,复位信号低电平的维持时间是否满足 PHY 的要求。

(4) 检查芯片的时钟频率、幅值、以及信号质量是否满足手册要求。不同类型的时钟输入方式原理是否正确。

(5)检查 PHY 芯片的地址、模式、电平配置等参数设置的上下拉电阻是否正常。

(6)检查 MDIO 总线的波形幅值、斜率、是否正常,振铃是否严重,是否有下降沿压降不到 0 的现象,在 MDC 的上升沿来临时 MDIO 的电平维持时间是否满足手册要求等问题。

先将芯片成功挂载后再看网口通不通。如果经过以上检测排查成功将 PHY 芯片挂载但是网口还是不通,那就需要继续检查数据接口部分。根据不同接口的规定来对数据接口的总线进行检查。

(1)按照具体的接口形式检查数据线连接是否正确,尤其是在使用MII,RMII,GMII,RGMII时的线序。检查参考时钟波形是否正常。

(2)检查网络变压器的中心抽头的接法是否与PHY 规定的网络变压器的驱动类型相对应。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

又重新搭了个个人博客

发表于 2024-04-26

哈喽大家好,我是咸鱼。

前段时间看到一个学弟写了篇用 Hexo 搭建博客的教程,心中沉寂已久的激情重新被点燃起来。(以前搞过一个个人网站,但是因为种种原因最后不了了之)

于是花了一天时间参考教程搭了个博客网站,我的博客网址是:xxxsalted.github.io/

下面是相关教程。

环境准备

俗话说:”工欲善其事必先利其器”,在搭建个人博客之前我们先要把环境准备好。

  • Github 准备

作为全球最大的程序员同性交友网站(不是),我们首先创建一个 Github 账号,官网地址:github.com

然后我们下载并安装 Git,官网地址: git-scm.com/downloads

毕竟是国外地址,我们访问起来可能比较慢,这时候我们可以访问下面的地址来下载

1
bash复制代码https://registry.npmmirror.com/binary.html?path=git-for-windows/v2.42.0.windows.2/

  • Node.js 准备

Node.js 简单来讲是一个 JavaScript 的运行环境,让我们的 JavaScript 代码不需要在浏览器上也能运行。

下载安装 Nodejs:nodejs.cn/download/

  • 环境配置

下载安装好 Git 和 Node.js 之后,我们在键盘上按下 win+R 键,输入 cmd。

然后在弹出的 cmd 窗口中输入下面的命令,来检验是否安装成功

1
2
3
bash复制代码git --version

npm version
  • 下载 cnpm

npm 是 Node.js 的安装包管理器,但是由于限制我们有时候通过 npm 下载东西会特别的慢,这时候我们就需要换一个国内的下载源并且使用 cnpm 来管理 Node.js。

1
bash复制代码npm install -g cnpm --registry=https://registry.npmmirror.com

初始化博客

  • 安装 Hexo

接下来我们要安装 Hexo ,还是在刚刚弹出的 cmd 窗口中输入:

1
bash复制代码cnpm install -g hexo
  • 创建新文件夹

我们在电脑任意位置创建一个新文件夹(最好别放在 C 盘下且文件名是英文),这个文件夹将用于储存你网站的静态文件。

比如说我的新建文件夹路径如下:

1
bash复制代码E:\myblog

然后右键鼠标,打开Git Bash Here,并依次输入以下命令:

1
2
3
4
5
6
7
8
9
csharp复制代码
# 初始化 hexo
hexo init

# 生成网页文件
hexo generate

# 部署到本地上
hexo server

在执行命令的时候出现权限相关问题(npm ERR! { Error: EPERM: operation not permitted, mkdir 'C:\Program Files\nodejs\node_cache\_locks'),可以参考下面这篇文章来解决:

www.cnblogs.com/yinxiangzhe…

然后你会发现你的目录下面多了很多文件:

接着打开下面网址验证一下:

1
bash复制代码http://localhost:4000/

如果你想停止运行,可以在上面的 bash 命令窗口中执行 Ctrl+C 命令。

到现在我们的个人博客就完成了一半了!

托管至 GitHub

  • 仓库创建

我们登录自己的 Github ,然后在 Github 中新建一个仓库。

注意: Repository name一定要按照图片上的格式填写,其他的默认即可。

  • 设置免密登录

为了方便我们本地与 Github 互相通信,我们需要设置 SSH 免密登录。

首先找到你的 Github 邮箱和用户名,然后在你个人博客的文件夹下右键鼠标,打开Git Bash Here,并依次输入以下命令:

1
2
3
bash复制代码git config --global user.name "github 用户名"

git config --global user.email "github 邮箱"

然后我们检验一下:

1
2
bash复制代码git config user.name
git config user.email

接着我们在键盘上按下 win+R 键,输入 cmd,在 cmd 窗口里面输入下面的命令:

1
2
bash复制代码# 检查本机是否已经存在 SSH 密钥
cd ~/.ssh

如果提示:No such file or directory 则输入如下代码,生成 SSH 文件:

1
bash复制代码ssh-keygen -t rsa -C "github 邮箱地址"

然后连续敲 3 次回车,最终会生成一个文件在“用户”目录下,打开 “用户” 目录(C:\用户\用户名.ssh)并找到id_rsa.p ub文件。

用“记事本”打开并复制里面的内容,在 Github 主页的右上角进入 Setting -> 左侧菜单栏 “SSH and GPG keys” -> New SSH Key -> 填写你获得的 SSH Key。

部署博客

到了这一步,我们就可以正式地把博客部署到 Git 上了,进入我们前面创建的博客文件夹,然后找到配置文件 _config.yml。

下面是我的配置文件的部分内容,大家可以参考一下

部署路径部分:

1
2
3
4
bash复制代码deploy:
type: git
repo: 你的 git 仓库地址
branch: master

网站相关部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
bash复制代码# Site
title: Amoon's blog
subtitle: ''
description: ''
keywords:
author: Ammon
language: zh-CN
timezone: 'Asia/shanghai'


## Set your site url here. For example, if you use GitHub Page, set url as 'https://username.github.io/project'
url:https://xxxsalted.github.io
root: /
permalink: :year/:month/:day/:title/
permalink_defaults:
pretty_urls:
trailing_index: true # Set to false to remove trailing 'index.html' from permalinks
trailing_html: true # Set to false to remove trailing '.html' from permalinks

我们还需要安装一个插件,打开 Git bash 窗口

1
2
bash复制代码# 安装自动部署工具
cnpm install hexo-deployer-git --save

然后依次执行下面的命令

1
2
3
4
5
6
7
8
bash复制代码# 清除 public 缓存文件
hexo clean

# 生成新的网站静态文件到默认设置的 public 文件夹,也可以用 hexo g 命令
hexo generate

# 部署到 github,也可以用 hexo d 命令
hexo deploy

当然,在生成静态文件之后你可以使用 hexo s 命令生成一个本地的 hexo 网站来做相关检验和测试,如果没什么问题就使用 hexo d 命令将其部署到 git 上

最后我们的博客就搭建成功啦!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

热度排名系统背后的技术:如何用Redis实现

发表于 2024-04-26

热度排名系统背后的技术:如何用Redis实现

在当今的互联网应用中,热度排名系统是一种非常常见的功能,它可以帮助用户发现热门内容,同时也为内容的推广和管理提供了一种有效的方式。在本篇技术博客中,我们将通过一个简单的案例,介绍如何使用Redis实现一个热度排名系统。

一、Redis简介

Redis是一个开源的内存数据结构服务器,它可以通过键值对的方式来处理数据。由于其出色的性能和丰富的数据类型,Redis被广泛应用于缓存、消息队列、排行榜等场景。

二、案例需求

假设我们需要为一个字符串集合中的每个字符实现一个热度排名,用户可以查看哪些字符的热度最高。

三、环境准备

首先,确保你的环境中安装了Node.js和Redis。接下来,安装ioredis模块,这是一个Node.js的Redis客户端库,它提供了一个简单易用的API来操作Redis。

1
复制代码npm install ioredis

四、实现热度排名

我们将通过以下步骤来实现热度排名:

  1. 创建Redis客户端实例:使用ioredis模块连接到Redis服务器。
  2. 生成随机数和随机字符:用于模拟字符的初始热度值和随机选择字符。
  3. 异步处理函数:定义一个异步函数jihe来处理热度值的更新和查询。

五、代码实现

以下是使用ioredis模块实现热度排名的完整代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
javascript复制代码const Redis = require('ioredis');
const redis = new Redis();

let num = Math.floor(Math.random() * 30) + 1; // 生成随机数1到30
let str = 'abcdefghkjlwer'; // 定义一个字符串
let strtap = Math.floor(Math.random() * 11); // 生成一个0到10的随机整数作为字符串的索引

async function jihe() {
 try {
   let data = await redis.zscore('hots', str[strtap]); // 查询有序集中特定成员的分数

   if (data !== null) {
     await redis.zincrby('hots', 1, str[strtap]); // 成员存在,分数增加1
     console.log(`${str[strtap]} 分数增加1`);
  } else {
     await redis.zadd('hots', num, str[strtap]); // 成员不存在,添加成员及其分数
     console.log(`写入新成员 ${str[strtap]},初始分数为 ${num}`);
  }

   let paixu = await redis.zrevrange('hots', 0, -1, 'WITHSCORES'); // 获取所有成员按分数从高到低排序的列表

   let obj = paixu.reduce((acc, val, index) => {
     if (index % 2 === 0) {
       acc[val] = parseFloat(paixu[index + 1]);
    }
     return acc;
  }, {});

   console.log(obj); // 打印转换后的对象
} catch (error) {
   console.error('An error occurred:', error);
}
}

jihe(); // 调用异步函数jihe

六、运行结果

运行上述代码后,控制台将输出每个字符的热度排名。初始时,会随机选择一个字符并为其分配一个初始热度值。随后,该字符的热度值每次增加1,模拟热度上升。

七、总结

通过本案例,我们学习了如何使用Redis的有序集合(sorted set)来实现一个简单的热度排名系统。这种系统可以轻松扩展,用于更复杂的应用场景,如社交媒体热门帖子、商品销量排行等。

Redis的高性能和低延迟特性使其成为实现这类系统的理想选择。随着对Redis更深入的探索,你将发现更多令人兴奋的可能性。


希望这篇技术博客能够帮助你理解如何使用Redis来实现热度排名系统。如果你有任何问题或想要了解更多关于Redis的内容,请随时留言讨论。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

释放Stable Diffusion 无限可能

发表于 2024-04-26

最近在整理大语言模型的系列内容,Stable Diffusion 是我下一篇博客的主题。关注 Stable Diffusion,是因为它是目前最受欢迎和影响力最大的多模态生成模型之一。Stable Diffusion 于 2022 年 8 月发布,主要用于根据文本的描述产生详细图像,当然它也可以应用于其他任务,比如内补绘制、外补绘制,以及在提示词指导下,对现有图形进行风格化或转变。Stable Diffusion 模型版本正在快速迭代,开源生态也在逐步扩展,对行业生产力带来了巨大的变革。如今出现了很多的开源软件,通过调用 Stable Diffusion 来支持各种功能,并提供简洁的用户界面以方便设计师和爱好者使用。然而 Stable Diffusion 的大规模部署不是一件简单的事情,需要考虑多种因素:

亚马逊云科技开发者社区为开发者们提供全球的开发技术资源。这里有技术文档、开发案例、技术专栏、培训视频、活动与竞赛等。帮助中国开发者对接世界最前沿技术,观点,和项目,并将中国优秀开发者或技术推荐给全球云社区。如果你还没有关注/收藏,看到这里请一定不要匆匆划过,点这里让它成为你的技术宝库!

  • 个性化和可扩展性:Stable Diffusion 生态广泛,仅广泛使用的基础模型就有 1.5 ,XL 和 2 三个版本。同时大量的插件和附加模型(如 LoRA ,ControlNet 等)可被附加到基础模型上。还可针对特定工作场景(如人像生成)进行精细化调优。这些模型和插件也都在不断迭代。在大规模部署时,针对不同的工作场景可以使用不同的模型进行模型推理,这对整个系统可扩展性要求很高。
  • 推理速度:Stable Diffusion 的基础模型是在亚马逊云计算服务上使用 256 个 NVIDIA A100 GPU 训练,共花费 15 万个 GPU 小时,成本为 60 万美元。而这只是基础模型,对于调用基础模型并加载个性化数据进行推理的应用场景来说,需要使用加速计算芯片(如 NVIDIA GPU 或亚马逊云科技的 Inferentia 芯片)来提升单任务推理速度,降低用户等待时间,提升用户体验。
  • 弹性伸缩:在多种业务场景中,使用者的请求有较大的不确定性。从平衡成本的角度出发,需要考虑在请求较多时快速增加推理实例数量以应对请求,而在请求较少时降低实例数量以降低成本。
    1.png

上图是一个常见的大语言模型在容器集群上的部署方式,这种部署方式存在以下问题:

  1. 所有请求都是同步的。由于模型推理相对比较耗时,每个请求耗时可达几十秒,甚至几分钟。这不仅要求客户与后端之间的网络绝对稳定,在流量突增且没有限流手段时,甚至会导致系统雪崩。
  2. 常见的自动扩容策略是基于 CPU 或 GPU 利用率的指标跟踪,无法直观反应系统负载,且触发时间长,无法应对突增请求。但如果为了避免冷启动而保留大量的空闲容量,则资源在低谷期大量闲置,空置成本高昂。
  3. 在弹性伸缩拉起新实例后,还需要加载 Stable Diffusion 运行时和模型才能对外提供服务。Stable Diffusion 运行时的容器镜像普遍在 10 GB 以上,新实例下载镜像和解压耗费的时间过长,导致冷启动时间过长,大大影响使用者的体验。
  4. Stable Diffusion 模型通常使用存放在块存储或文件存储中,每次加载模型时候拉取性能受限,成本也较高。
解决方案

为了解决传统部署方式带来的业务痛点,亚马逊云科技中国区的技术团队基于使用者的需求和对多种业务场景的深刻理解提出了一套解决方案。该方案充分利用云上资源的弹性,无服务器产品的简便,和 Kubernetes 生态强大的任务调度能力,展示了如何构建端到端低成本、快速扩展的异步图像生成架构。

  • 拥抱开源生态:支持 Stable Diffusion Web UI 和 ComfyUI 两种开源的 Stable Diffusion 运行时,和基于这两种运行时的插件生态。
  • 更快速的扩容:基于 KEDA、Karpenter、Bottlerocket,提供可自定义的,任务粒度的自动扩容能力。最快可以达到 1 分钟内业务就绪,适合高并发,且对响应时间有要求的情况。
  • 大幅降低成本:通过使用 Amazon EC2 Spot 实例和 Mountpoint for Amazon S3 CSI Driver,使得计算成本相较于 EC2 标准价格可节省最高约 70%,多节点场景下的存储成本相较于其他存储方案更可节省大半。
  • 扩展非常方便:该方案是云原生架构,基于 Amazon EKS、无服务器(Serverless)和事件驱动(Event-Driven)架构,大部分组件无需自行管理,且便利扩展。
  • 可视化监控:提供调用流程详细的可视化,历史统计功能,提供完善的可观测性组件,有效缩短 MTTI(Mean Time to Identify)。
  • 自动化部署:方案使用 Amazon CDK,借助编程语言的强大表达能力和高效执行效率,仅需 30-40 分钟就可以自动完成构建。
  • Stable DiffusionWeb UI

github.com/AUTOMATIC11…

  • ComfyUI

github.com/comfyanonym…

  • KEDA

keda.sh/?trk=cndc-d…

  • Karpenter

karpenter.sh/?trk=cndc-d…

  • Bottlerocket

aws.amazon.com/cn/bottlero…

  • Amazon EC2 Spot 实例

aws.amazon.com/cn/ec2/spot…

  • Mountpoint for Amazon S3 CSI Driver

github.com/awslabs/mou…

  • Amazon EKS

aws.amazon.com/cn/eks/?trk…

  • Amazon CDK

aws.amazon.com/cn/cdk/?trk…

方案的具体工作流程如下图所示:

2.png

  1. 使用者将请求(模型,Prompt 等)发送至业务应用,业务应用将请求发送至 Amazon API Gateway 提供的 API 端点。
  2. 请求通过 Amazon Lambda 进行校验,并投送至 Amazon SNS 主题。
  3. Amazon SNS 根据请求中的运行时名称,基于请求过滤机制,将请求投送至对应运行时的 SQS 队列。
  4. 在 EKS 集群中,KEDA 会根据队列内消息数量扩充运行时的副本数。
  5. Stable Diffusion 运行时启动时会通过 Mountpoint for Amazon S3 CSI Driver,直接从 S3 存储桶中加载模型。
  6. Queue Agent 会从 Amazon SQS 队列里接收任务,并发送给 Stable Diffusion 运行时生成图像。
  7. 生成的图片由 Queue Agent 存储至 Amazon S3 存储桶中,并将完成通知投送至 Amazon SNS 主题,SNS 可将响应投送至 SQS 或其他目标中。
  8. 该解决方案提供完整的可观测性和管理组件,包含基于 CloudWatch 和 ADOT 的数值监控和日志,以及基于 Amazon X-Ray 的全链路跟踪。
  9. 该解决方案通过基于 Amazon CDK 的基础设施即代码部署方式进行部署和配置,通过 IAM 和 API Key 提供安全和访问控制。
方案的生产实践
API 接入和事件分发

该解决方案使用基于 Amazon API Gateway 的 API 端点对外提供服务,并基于 Amazon SNS,Amazon SQS 进行任务分发。用户将请求(模型,Prompt 等)发送至 Amazon API Gateway 提供的 API 端点,请求通过 Amazon Lambda 进行校验,并投送至 Amazon SNS 主题。Amazon SNS 根据请求中的运行时名称,将请求投送至对应运行时的 SQS 队列。

3.png

使用 Queue Agent 组件进行异步推理

部署时,每个运行时有独立的 Amazon SQS 队列以接收请求。而考虑到不同的的 Stable Diffusion 运行时的 API 接口和行为不同,对异步处理和队列的支持也不同,故我们构建了名为 Queue Agent 的组件。Queue Agent 会从 Amazon SQS 队列里接收任务,并转换成对应运行时的 API 请求格式,发送给 Stable Diffusion 运行时生成图像。Queue Agent 还兼顾了从 Internet 或 Amazon S3 上获取图像,和任务跟踪,错误处理的功能,将更多任务从 Stable Diffusion 运行时解耦,从而支持更多的 Stable Diffusion 运行时。

4.png

利用 KEDA 自动扩缩容器副本

当 Amazon SQS 队列中积压过多消息时,需要增加容器副本数以快速处理这些积压任务。我们使用开源的 Kubernetes Event-Driven Autoscaling(简称 KEDA)进行弹性伸缩。KEDA 会轮询 SQS 队列的队列内消息数量,并根据消息数量和预设的阈值来决定运行时的副本数。由于 KEDA 持续监控队列,在没有负载的时候,可以不启动任何运行时,而在有任务时再启动运行时实例,有效节省成本。除了 SQS 队列长度外,KEDA 还支持根据许多其他指标(被称为 Scaler)进行扩缩容。例如部分业务场景有明确的业务高峰期,则可以使用 Cron 在预估的业务高峰期提前进行扩容,而在业务低谷期不预置资源。通过多个 Scaler 的组合,可以支持更多的工作场景。

  • Kubernetes Event-Driven Autoscaling

keda.sh/?trk=cndc-d…

5.png

利用 Karpenter 自动扩缩实例

在运行时副本数扩展后,需要节点弹性伸缩组件创建实例承载新的运行时副本。传统的节点弹性伸缩组件是 Kubernetes 社区的 Cluster Autoscaler。而亚马逊云科技开源的弹性伸缩组件 Karpenter 可以智能选择实例类型和购买方式。根据同时启动的 Pod 数量,选择最合适的实例类型。例如在同时启动的 Pod 数量很多时,可以选择包含 2 张或 4 张 GPU 的实例,减少实例启动时间对冷启动时间的影响。Karpenter 使用 EC2 Fleet,实例启动速度快于基于 Autoscaling Group 的节点组。在推理任务的成本划分中,GPU 实例的成本占了总成本的 95% 以上。Karpenter 支持创建 Spot 实例,且在创建时会智能选择回收几率最低的实例类型,降低 Spot 实例回收对任务造成的影响。

  • Cluster Autoscaler

github.com/kubernetes/…

6.png

基于 Bottlerocket 实现容器镜像缓存

在存储方面,方案针对 Stable Diffusion 运行时和模型使用了不同的存储。新创建的实例采用了 Bottlerocket 操作系统。Bottlerocket 是一款基于 Linux 的开源操作系统,由亚马逊云科技专门构建用于运行容器。Bottlerocket 仅包括运行容器所需的基本软件,启动速度极快。而在 Stable Diffusion 运行时镜像方面,由于 Docker 拉取镜像是单线程,且拉取完成后还需要解压缩,导致镜像拉取耗时较长。我们将 Stable Difussion 运行时镜像预加载到磁盘,并抓取 EBS 快照。在每次创建新实例时,EBS 快照会恢复到新实例的数据盘,新的实例在加入 EKS 集群后可以立即启动容器,节省拉取镜像和解压的时间。

7.png

利用对象存储支撑大量模型动态读取

常见的模型存储会使用块存储或者文件存储,但对于模型读取这种连续读的工作负载,挂载对象存储存在优势。Mountpoint for Amazon S3 可以将 S3 存储桶挂载到文件系统。在读取模型时支持多线程读取,充分利用网卡资源。在 Amazon S3 上的性能测试显示,使用 Mountpoint for Amazon S3 加载 Stable Diffusion 1.5 的基础模型仅需 14.5 秒,进一步缩减冷启动耗时。

8.png

推送处理结果

生成的图片由 Queue Agent 存储至 Amazon S3 存储桶中,并将完成通知投送至 Amazon SNS 主题。不论任务成功或失败,运行时的返回也会存储到 S3 存储桶,方便进行调试。而只有任务真正处理完成后,Queue Agent 才会从队列中删除对应的任务。当出现意外状况(如 SD 运行时崩溃)时,任务会在设定的一段时间(被称为 Visibility Timeout)后,重新出现在队列中,其他运行时可以重试这个任务。Amazon SQS 这个“两阶段确认”的特性有效的提升了整体方案的可靠性。

9.png

X-Ray 端到端全链路追踪

而在可观测性方面,除了传统的基于 CloudWatch 数值监控和日志之外,通过分布式跟踪,我们能了解每一步的耗时和性能瓶颈,方便运维人员快速排查问题。

10.png

方案的性能与成本

这套方案已经在多个 Amazon Web Services 使用者的生产环境上部署。而我们在 Amazon Web Services 美国西部(俄勒冈)区域的内部实测,端到端的扩容速度(即请求发送到处理完成)最低可达 60 秒,完全可以有效应对需要快速扩容的场景。推理成本由于使用了 Spot 实例最高可节省 70%,有效降低了方案的整体成本,适应大规模部署。

开始使用

目前这套方案已经开源,支持部署到亚马逊云科技海外和中国区域。您可以从 github.com/aws-samples… 获取源代码并开始在您的环境中部署。

资源与参考文献:

  • aws.amazon.com/cn/solution…
  • aws-samples.github.io/stable-diff…

文章来源:dev.amazoncloud.cn/column/arti…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Swift 6:导入语句上的访问级别

发表于 2024-04-26

前言

SE-0409 提案引入了一项新功能,即允许使用 Swift 的任何可用访问级别标记导入声明,以限制导入的符号可以在哪些类型或接口中使用。由于这些变化,现在可以将依赖项标记为对当前源文件(private 或 fileprivate)、模块(internal)、包(package)或所有客户端(public)可见。

此提案引入了两个功能标志后面的更改,这两个功能标志将在 Swift 6 中默认启用:

  • AccessLevelOnImport:这是一个已经可用的实验性功能标志,允许开发人员将导入声明标记为访问级别。
  • InternalImportsByDefault:这是一个即将推出的功能标志,目前尚不可用,它将导入语句的隐式访问级别从 public 更改为 internal,就像 Swift 6 将要做的那样。

这是语言中的一项很好的补充,我个人很长时间以来一直期待着,因为它可以帮助开发人员更好地隐藏实现细节并强制执行关注点分离。不仅如此,它还限制了包的客户端导入的依赖项数量,只允许满足一定条件的标记为 public 的依赖项导入,从而缩短了编译时间。

示例

假设我们创建了一个名为 Services 的 Swift 包,该包定义了一个 FeedService 目标。该目标的工作是获取要在应用程序中显示的项目的动态源。反过来,FeedService 依赖于另一个名为 FeedDTO 的目标,该目标定义了与 API 数据结构匹配的一组自动生成的可解码模型,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
swift复制代码// swift-tools-version: 5.10

import PackageDescription

let package = Package(
name: "Services",
platforms: [.iOS(.v13), .macOS(.v10_15)],
products: [
.library(
name: "FeedService",
targets: ["FeedService"]
),
],
targets: [
.target(
name: "FeedService",
dependencies: ["FeedDTO"]
),
.target(
name: "FeedDTO"
)
]
)

FeedDTO 目标的代码非常简单,并且是基于 OpenAPI 规范自动生成的:

1
2
3
4
5
6
7
8
9
10
11
swift复制代码import Foundation

public struct Feed: Decodable {
let items: [Item]

public struct Item: Decodable {
let title: String
let image: URL
let body: String
}
}

FeedService 目标并不复杂,它包含一个协议,该协议定义了服务的接口,供客户端使用。该协议的实现也属于 FeedService 目标,但对于本例来说并不重要,FeedService.swift 文件代码如下:

1
2
3
4
5
swift复制代码import FeedDTO

public protocol FeedService {
func fetch() -> Feed
}

正如你所看到的,我们在服务的公共接口中包含了 FeedDTO 目标中的 Feed 模型。由于在 Swift 5 中,所有导入声明都隐式为 public,并且没有办法更改此行为,上述代码可以编译而不会出现任何问题。尽管如此,架构远非理想,我们被允许暴露实现细节,并且我们没有办法让编译器阻止此泄漏。

如果我们注意到这个问题并想要解决它,我们可以从公共接口中删除 Feed 模型,并创建一个领域模型,该模型将成为公共接口的一部分。服务的实际实现将负责将 FeedDTO.Feed 模型转换为领域模型。FeedService.swift 文件代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
swift复制代码import Foundation
import FeedDTO

public struct Feed {
let items: [Item]

public struct Item {
let title: String
let image: URL
let body: String
}
}

public protocol FeedService {
func fetch() -> Feed
}

尽管上述代码是朝着正确方向迈出的一步,但代码中没有明确说明 FeedDTO 模块在此文件中的用法是实现细节,不应该是模块的公共接口的一部分。这就是 Swift 6 的功能派上用场的地方。

启用 AccessLevelOnImport

启用 AccessLevelOnImport 实验性标志

让我们看看如何通过为导入语句添加访问级别来使前一节的代码更加明确,并防范未来的更改可能会在此文件中暴露实现细节。

在我们这样做之前,由于此功能仍在实验性标志后面,我们需要在我们的Swift包中启用它,Package.swift 文件代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
swift复制代码// swift-tools-version: 5.10

import PackageDescription

let package = Package(
name: "FeedService",
platforms: [.iOS(.v13), .macOS(.v10_15)],
products: [
.library(
name: "FeedService",
targets: ["FeedService"]
),
],
targets: [
.target(
name: "FeedService",
dependencies: ["FeedDTO"],
swiftSettings: [
.enableExperimentalFeature("AccessLevelOnImport")
]
),
.target(name: "FeedDTO")
]
)

如果你使用的是 Xcode 项目,则可以通过将 -enable-experimental-feature AccessLevelOnImport 标志添加到目标的 OTHER_SWIFT_FLAGS 构建设置中来启用该功能。

现在我们已经启用了该功能,我们可以在 FeedService.swift 文件中的导入语句中添加访问级别,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
swift复制代码import Foundation
private import FeedDTO

public struct Feed {
let items: [Item]

public struct Item {
let title: String
let image: URL
let body: String
}
}

public protocol FeedService {
func fetch() -> Feed
}

通过这个改变,如果我们再次在模块的公共接口中使用 FeedDTO,编译器将会报错。这是一种强制实现关注点分离和隐藏模块客户端的实现细节的绝佳方式。

请注意,你可以在同一个依赖项在目标中使用不同的访问级别。在执行优化和决定是否将依赖项带给模块的消费者时,构建系统将考虑最不限制的访问级别。

破坏性变更

与 SE-0409 引入的更改相关的一个重大破坏性变更是:导入语句的默认访问级别将从 public 更改为 internal。这意味着,如果你在模块的公共接口中包含来自依赖项的符号,你需要明确将导入语句标记为 public,以避免编译错误。

有一个第二个功能标志,你很快就可以在 Swift 工具链的主要分支上启用,称为 InternalImportsByDefault,以测试新的行为。当它正式发布时,你将能够在你的 Swift 包中启用它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
swift复制代码// swift-tools-version: 5.10

import PackageDescription

let package = Package(
name: "FeedService",
platforms: [.iOS(.v13), .macOS(.v10_15)],
products: [
.library(
name: "FeedService",
targets: ["FeedService"]
),
],
targets: [
.target(
name: "FeedService",
dependencies: ["FeedDTO"],
swiftSettings: [
.enableExperimentalFeature("AccessLevelOnImport"),
.enableUpcomingFeature("InternalImportsByDefault")
]
),
.target(name: "FeedDTO")
]
)

如果你使用的是 Xcode 项目,则可以通过将 -enable-upcoming-feature InternalImportsByDefault 标志添加到目标的 OTHER_SWIFT_FLAGS 构建设置中来启用该功能。

采用这些更改

在采用这些新更改时的最佳实践是首先在你的 Swift 包中启用 AccessLevelOnImport 功能标志,并开始将最严格的访问级别添加到所有的导入语句中,让编译器告诉你可能需要进行更改的地方。

这是一个为你执行此操作的小脚本,replace-imports.swift 文件代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
swift复制代码#!/usr/bin/swift

private import Foundation

let fileManager = FileManager.default
let currentDirectory = fileManager.currentDirectoryPath
let swiftFiles = fileManager.enumerator(atPath: currentDirectory)?
.compactMap { $0 as? String }
.filter { $0.hasSuffix(".swift") }

for file in swiftFiles ?? [] {
let filePath = "\(currentDirectory)/\(file)"
guard let content = try? String(contentsOfFile: filePath) else {
continue
}

let updatedContent = content
.replacingOccurrences(of: #"import (\w+)"#, with: "private import $1", options: .regularExpression)

try? updatedContent.write(toFile: filePath, atomically: true, encoding: .utf8)
}

如果你对你的公共接口和它们所暴露的内容感到满意,或者如果你发现当你打开 InternalImportsByDefault 即将推出的功能标志时,有很多编译错误你不想立即修复,你可以修改上述脚本以将 public 访问级别添加到所有导入语句中。

总结

该文章介绍了 Swift 6 中关于导入声明访问级别的新功能。SE-0409 提案引入了此功能,允许开发人员使用任何可用的访问级别标记导入声明,从而限制了导入的符号在哪些类型或接口中可以使用。这项功能通过两个功能标志实现,即 AccessLevelOnImport 和 InternalImportsByDefault,它们将在 Swift 6 中默认启用。文章通过示例说明了如何在 Swift 包中使用这些功能,并介绍了相关的破坏性变更。最后,文章提出了采用这些更改的最佳实践,并提供了一个小脚本来帮助开发人员执行相应的更改。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

从‘海量请求’到‘丝滑响应’:巧解海量httpclient请

发表于 2024-04-26
大家好,我是石头~


今天接到一个需求,其关键功能需要依托一个外部HTTP接口来实现,而且这项功能还是整个业务中请求频率最高的。


开完需求会后,我就开始头疼了。众所周知,在高并发场景下进行HTTP请求,会降低整个服务的性能,怎样进行性能优化,就成为了实现本次需求的核心了。

image.png

1、大量HTTP请求的弊端

在进行性能优化之前,我们先来了解下为什么大量HTTP请求,会造成服务性能下降。


这个我们可以从以下几方面来看:
  1. 网络资源竞争:服务端在向其他服务发起HTTP请求时,会产生网络带宽的竞争。特别是当请求量很大时,大量的数据包在网络中穿梭,容易导致网络带宽饱和,增加延迟,甚至产生网络拥塞,使得请求响应时间延长。
  2. 系统资源消耗:服务端在处理每个HTTP请求时,都需要占用CPU、内存、文件句柄等系统资源。特别是在并发请求较高时,服务端必须创建和维护多个连接,处理请求和解析响应,这些都会消耗大量系统资源。一旦资源耗尽,新进的请求将无法得到及时处理,严重影响服务性能。
  3. 高并发下的连接管理:对于每次HTTP请求,服务端通常需要创建一个新的TCP连接。如果连接创建和销毁过于频繁,会大大增加系统开销。如果不采取连接池等优化手段,服务端可能会因连接管理负担过重而降低性能。
  4. 请求排队与响应时间:服务端发起的HTTP请求也需要排队等待对方服务器的响应,尤其是在目标服务本身负载较大或者网络条件不佳的情况下,响应时间的增长将进一步拖慢服务端的处理速度,最终可能形成连锁反应,导致整个系统性能下降。
综上所述,大量HTTP请求就像高峰期的交通堵塞,不仅挤占了网络通道,也给服务器处理能力带来巨大挑战。


那么,针对这些问题,我们要怎样进行优化?

b812c8fcc3cec3fd17b0f03685f7ff3286942777.webp

2、大量HTTP请求的优化策略

由于团队内部采用的是httpclient,那接下来,我们就以httpclient为例进行优化。

策略一:连接池管理

如同高效有序的物流仓库,高效的httpclient请求离不开合理的连接池管理。通过设置合适的最大连接数、超时时间以及重试策略,我们可以避免频繁创建和关闭连接带来的性能损耗,同时也能应对突发的大流量请求。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码    PoolingHttpClientConnectionManager connMgr = new PoolingHttpClientConnectionManager();
connMgr.setMaxTotal(200); // 设置最大连接数
connMgr.setDefaultMaxPerRoute(20); // 设置每个路由基础的默认最大连接数

RequestConfig requestConfig = RequestConfig.custom()
.setSocketTimeout(5000) // 设置SO_TIMEOUT,即从连接成功建立到读取到数据之间的等待时间
.setConnectTimeout(3000) // 设置连接超时时间
.setConnectionRequestTimeout(1000) // 设置从连接池获取连接的等待时间
.build();

CloseableHttpClient httpClient = HttpClients.custom()
.setConnectionManager(connMgr)
.setDefaultRequestConfig(requestConfig)
.build();

策略二:异步化处理与线程池

面对大量的网络请求,同步处理方式可能会导致线程阻塞,影响整体性能。采用异步处理机制结合线程池技术,能够将请求放入队列并分配给空闲线程执行,从而大大提高系统的并发处理能力,降低响应时间。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
java复制代码    // 创建线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
10, // 核心线程数
20, // 最大线程数
60, // 空闲线程存活时间(单位秒)
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100)); // 工作队列

CloseableHttpClient httpClient = ...; // 初始化 HttpClient

List<String> urls = ...; // 要请求的URL列表

for (String url : urls) {
final String finalUrl = url;
Runnable task = () -> {
try (CloseableHttpResponse response = httpClient.execute(new HttpGet(finalUrl))) {
// 处理响应逻辑
} catch (IOException e) {
// 处理异常
}
};

executor.execute(task);
}

// 在所有任务完成后关闭线程池
executor.shutdown();
// 可以选择等待所有任务完成
executor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);

// 或者在特定条件下停止并强制关闭线程池
if (!executor.isTerminated()) {
executor.shutdownNow();
}

// 别忘了在最后关闭HttpClient资源
httpClient.close();

策略三:请求合并与批量处理

对于类似的或依赖关系不强的请求,可以考虑合并为一个请求或者批量处理,减少网络交互次数,显著提升效率。例如,利用HTTP/2的多路复用特性,或者对数据进行归类整合后一次性获取。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码    List<String> userIds = ...; // 用户ID列表

HttpUriRequest[] requests = userIds.stream()
.map(userId -> new HttpGet("http://example.com/api/user/" + userId))
.toArray(HttpUriRequest[]::new);

HttpRequestRetryHandler retryHandler = ...; // 自定义重试处理器
HttpClient httpClient = HttpClients.custom().setRetryHandler(retryHandler).build();

List<Future<HttpResponse>> futures = new ArrayList<>();
for (HttpUriRequest request : requests) {
futures.add(httpClient.execute(request, new FutureCallback<HttpResponse>() {...}));
}

// 等待所有请求完成并处理结果
for (Future<HttpResponse> future : futures) {
HttpResponse response = future.get();
// 处理每个用户的响应信息
}

策略四:缓存优化

对于部分不变或短期内变化不大的数据,可以通过本地缓存或分布式缓存(如Redis)来避免重复请求,既节省了带宽,也减轻了服务器压力。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码    LoadingCache<String, String> cache = CacheBuilder.newBuilder()
.maximumSize(1000) // 设置缓存的最大容量
.expireAfterWrite(1, TimeUnit.HOURS) // 数据写入一小时后过期
.build(new CacheLoader<String, String>() {
@Override
public String load(String key) throws Exception {
// 如果缓存中没有该key,则通过httpclient请求获取数据并返回
CloseableHttpResponse response = httpClient.execute(new HttpGet("http://example.com/api/data/" + key));
return EntityUtils.toString(response.getEntity());
}
});

// 获取数据,如果缓存中有则直接返回,否则发起网络请求并将结果存入缓存
String data = cache.get("someKey");

3、结语

优化之路永无止境,每一个环节都可能存在更深层次的改进空间,希望上面的内容在当你遇到类似问题时,对你有所帮助~

**MORE | 更多精彩文章**

  • 面试官:说说单点登录都是怎么实现的?
  • 面试不慌张:一文读懂FactoryBean的实现原理
  • JWT vs Session:到底哪个才是你的菜?
  • JWT重放漏洞如何攻防?你的系统安全吗?
  • JWT:你真的了解它吗?

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

如何在网页实现 TypeScript 编辑器?

发表于 2024-04-26

有的需求需要在网页上写代码。

比如在线执行代码的 playground:

或者在线面试:

如果让你实现网页版 TypeScript 编辑器,你会如何做呢?

有的同学说,直接用微软的 monaco editor 呀:

确实,直接用它就可以,但是有挺多地方需要处理的。

我们来试试看。

1
lua复制代码npx create-vite

创建个 vite + react 的项目。

安装依赖:

1
2
3
bash复制代码npm install

npm install @monaco-editor/react

这里用 @monaco-editor/react 这个包,它把 monaco editor 封装成了 react 组件。

去掉 main.tsx 里的 index.css

然后在 App.tsx 用一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
javascript复制代码import MonacoEditor from '@monaco-editor/react'

export default function App() {

const code = `import lodash from 'lodash';
function App() {
return <div>guang</div>
}
`;

return <MonacoEditor
height={'100vh'}
path={"guang.tsx"}
language={"typescript"}
value={code}
/>
}

跑下开发服务:

1
arduino复制代码npm run dev

试下看:

现在就可以在网页写 ts 代码了。

但是有报错:

jsx 语法不知道怎么处理。

这里明显要改 typescript 的 tsconfig.json。

怎么改呢?

这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
javascript复制代码import MonacoEditor, { OnMount } from '@monaco-editor/react'

export default function App() {

const code = `import lodash from 'lodash';
function App() {
return <div>guang</div>
}
`;

const handleEditorMount: OnMount = (editor, monaco) => {
monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
jsx: monaco.languages.typescript.JsxEmit.Preserve,
esModuleInterop: true,
})

}

return <MonacoEditor
height={'100vh'}
path={"guang.tsx"}
language={"typescript"}
onMount={handleEditorMount}
value={code}
/>
}

onMount 的时候,设置 ts 的 compilerOptions。

这里设置 jsx 为 preserve,也就是输入

输出
,保留原样。

如果设置为 react 会输出 React.createElement(“div”)。

再就是 esModuleInterop,这个也是 ts 常用配置。

默认 fs 要这么引入,因为它是 commonjs 的包,没有 default 属性:

1
javascript复制代码import * as fs from 'fs';

设置 esModuleInterop 会在编译的时候自动加上 default 属性。

就可以这样引入了:

1
javascript复制代码import fs from 'fs';

可以看到,现在 jsx 就不报错了:

还有一个错误:

没有 lodash 的类型定义。

写 ts 代码没提示怎么行呢?

我们也要支持下。

这里用到 @typescript/ata 这个包:

ata 是 automatic type acquisition 自动类型获取。

它可以传入源码,自动分析出需要的 ts 类型包,然后自动下载。

我们新建个 ./ata.ts,复制文档里的示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
javascript复制代码import { setupTypeAcquisition } from '@typescript/ata'
import typescriprt from 'typescript';

export function createATA(onDownloadFile: (code: string, path: string) => void) {
const ata = setupTypeAcquisition({
projectName: 'my-ata',
typescript: typescriprt,
logger: console,
delegate: {
receivedFile: (code, path) => {
console.log('自动下载的包', path);
onDownloadFile(code, path);
}
},
})

return ata;
}

安装用到的包:

1
css复制代码npm install --save @typescript/ata -f

这里就是用 ts 包去分析代码,然后自动下载用到的类型包,有个 receivedFile 的回调函数里可以拿到下载的代码和路径。

然后在 mount 的时候调用下:

1
2
3
4
5
6
7
8
9
javascript复制代码const ata = createATA((code, path) => {
monaco.languages.typescript.typescriptDefaults.addExtraLib(code, `file://${path}`)
})

editor.onDidChangeModelContent(() => {
ata(editor.getValue());
});

ata(editor.getValue());

就是最开始获取一次类型,然后内容改变之后获取一次类型,获取类型之后用 addExtraLib 添加到 ts 里。

看下效果:

有类型了!

写代码的时候用到的包也会动态去下载它的类型:

比如我们用到了 ahooks,就会实时下载它的类型包然后应用。

这样,ts 的开发体验就有了。

再就是现在字体有点小,明明内容不多右边却有一个滚动条:

这些改下 options 的配置就好了:

scrollBeyondLastLine 是到了最后一行之后依然可以滚动一屏,关闭后就不会了。

minimap 就是缩略图,关掉就没了。

scrollbar 是设置横向纵向滚动条宽度的。

theme 是修改主题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
javascript复制代码return <MonacoEditor
height={'100vh'}
path={"guang.tsx"}
language={"typescript"}
onMount={handleEditorMount}
theme: "vs-dark",
value={code}
options={
{
fontSize: 16,
scrollBeyondLastLine: false,
minimap: {
enabled: false,
},
scrollbar: {
verticalScrollbarSize: 6,
horizontalScrollbarSize: 6,
}
}
}
/>

好多了。

我们还可以添加快捷键的交互:


默认 cmd(windows 下是 ctrl) + j 没有处理。

我们可以 cmd + j 的时候格式化代码。

1
2
3
javascript复制代码editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyJ, () => {
editor.getAction('editor.action.formatDocument')?.run()
});

试下效果:

有同学可能问,monaco editor 还有哪些 action 呢?

打印下就知道了:

1
2
3
4
5
javascript复制代码editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyJ, () => {
// editor.getAction('editor.action.formatDocument')?.run()
let actions = editor.getSupportedActions().map((a) => a.id);
console.log(actions);
});

有 131 个:

用到再搜就行。

这样,我们的网页版 TypeScript 编辑器就完成了。

总结

有的需求需要实现网页版编辑器,我们一般都用 monaco editor 来做。

今天我们基于 @monaco-editor/react 实现了 TypeScript 编辑器。

可以在 options 里配置滚动条、字体大小、主题等。

然后 onMount 里可以设置 compilerOptions,用 addCommand 添加快捷键等。

并且我们基于 @typescript/ata 实现了自动下载用到的 ts 类型的功能,它会扫描代码里的 import,然后自动下载类型,之后 addExtraLib 添加到 ts 里。

这样在网页里就有和 vscode 一样的 ts 编写体验了。

为啥要研究这个呢?

因为我最近在开发 react playground,在左侧写代码,然后实时编译在右侧预览:

这是我小册 《React 通关秘籍》的一个项目,感兴趣的话可以上车一起做。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Android TextView的颜色和字体自适应

发表于 2024-04-26

前言

最近比较忙,没有时间去梳理一些硬核的东西,今天就来分享一些简单有意思的技巧。拿TextView来说,平时我们都会在特定的场景去设置它的字体颜色和字体大小。那么有没有一种办法能够一劳永逸不用每次都去设置,能让TextView自己去根据自身的控件属性去自适应颜色和大小呢?当然是有的,这里可以简单的分享一些思路。

1. 字体大小自适应

TextView可以根据让字体的大小随着宽高进行自适应。

设置大小自适应的方式很简单,只需要添加这3行代码即可

1
2
3
ini复制代码android:autoSizeMaxTextSize="22dp"  
android:autoSizeMinTextSize="8dp"
android:autoSizeTextType="uniform"

我们可以来看看效果,我给宽高都设置不同的值,能看到字体大小变化的效果

1
2
ini复制代码android:layout_width="50dp"  
android:layout_height="20dp"

image.png

1
2
ini复制代码android:layout_width="50dp"  
android:layout_height="30dp"

image.png

1
2
ini复制代码android:layout_width="50dp"  
android:layout_height="50dp"

image.png

1
2
ini复制代码android:layout_width="80dp"  
android:layout_height="80dp"

image.png

最后这里可以看到autoSizeMaxTextSize的效果

这里可以多提一句,一般这种字体随宽高自适应的场景在正常开发中比较少见。如果你的项目合理的话,一般字体的大小都是固定那几套,所以把字体大小定义到资源文件中,甚至通过style的方式去设置,才是最节省时间的方式。

2. 字体颜色自适应

关于字体的颜色自适应,如果你真想把这套东西搞起来,你就需要对“颜色”这个概念有一定的深层次的了解。我这里就只简单做一些效果来举例。

我这里演示Textview根据背景颜色来自动设置字体颜色是白色还是黑色,当背景颜色是暗色时(比如黑色),字体颜色变成白色,当背景颜色是亮色时(比如白色),字体颜色变成黑色。

那么首先需要有个概念:我怎么判断背景是亮色还是暗色?

这就需要对颜色有一定的理解。要判断一个颜色是暗色还是亮色,可以通过计算颜色的亮度来实现。一种常见的方法是将RGB颜色值转换为灰度值,然后根据灰度值来判断颜色的深浅程度。

灰度值的计算公式 灰度值 = 0.2126 * R + 0.7152 * G + 0.0722 * B

根据这个公式,我们能封装一个判断颜色是否是亮色的方法

1
2
3
4
5
6
7
kotlin复制代码private fun isLightColor(color: Int): Boolean {  
val r = color shr 16 and 0xFF
val g = color shr 8 and 0xFF
val b = color and 0xFF
val luminance = (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255
return luminance > 0.5
}

如果觉得这个判断不太符合你心里的预期,可以修改最后一行的luminance > 0.5值

下一步,我们需要获取控件的背景,然后从背景中获取颜色值。

获取背景直接调用

1
ini复制代码val d = textView?.background

根据Drawable去获取颜色

1
2
3
4
5
6
7
8
9
10
11
kotlin复制代码private fun getColorByDrawable(d : Drawable) : Int{  
val bitmap = Bitmap.createBitmap(
textView?.width ?: 0,
textView?.height ?: 0,
Bitmap.Config.ARGB_8888
)
val canvas = Canvas(bitmap)
d.setBounds(0, 0, canvas.width, canvas.height)
d.draw(canvas)
return bitmap.getPixel(0, 0)
}

注意,我这里不考虑渐变色的情况,只是考虑单色的情况,所以x和y是传0,一般对于复杂的渐变色也不好做适配,但是对于background分边框和填充两种颜色的情况,一般文字都是显示在填充区域,这时候的x和y可以去根据边框宽度去加个偏移量(总之可以灵活应变)

还有一种场景,对于TextView没背景颜色,是它的父布局有背景颜色的情况,可以循环去调用父布局的view.background判断是否为空,为空就循环一次,不为空直接获取颜色。我这里就不演示代码了。

这里先把全部代码贴出来(都是用了最简单的方式)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
kotlin复制代码override fun onCreate(savedInstanceState: Bundle?) {  
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_demo_text)

textView = findViewById(R.id.tv)
val d = textView?.background
textView?.post {
if (d != null){
if (isLightColor(getColorByDrawable(d))){
textView?.setTextColor(resources.getColor(R.color.black))
}else{
textView?.setTextColor(resources.getColor(R.color.white))
}
}
}
}

private fun getColorByDrawable(d : Drawable) : Int{
val bitmap = Bitmap.createBitmap(
textView?.width ?: 0,
textView?.height ?: 0,
Bitmap.Config.ARGB_8888
)
val canvas = Canvas(bitmap)
d.setBounds(0, 0, canvas.width, canvas.height)
d.draw(canvas)
return bitmap.getPixel(0, 0)
}

private fun isLightColor(color: Int): Boolean {
val r = color shr 16 and 0xFF
val g = color shr 8 and 0xFF
val b = color and 0xFF
val luminance = (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255
return luminance > 0.5
}

然后改几个背景色来看看效果

1
ini复制代码android:background="#000000"

image.png

1
ini复制代码android:background="#ffffff"

image.png

1
ini复制代码android:background="#3377ff"

image.png

1
ini复制代码android:background="#ee7700"

image.png

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

PostgerSQL技术问答-00 Why Postgres

发表于 2024-04-26

笔者关于Nodejs的还有很多的坑还没有填上,这里又打算开一个新的坑系列-PostgreSQL,因为它们都是在Web应用开发中常用的技术。这里也不是写书,写博客就可以更随意一点。内容的碎片化不可避免,但只能先这样。可能等到时机成熟的时候,再重新组织整理一下。

这个系列的主题是:《PostgerSQL使用问答》,主要是总结针对在开发中遇到的一些PostgreSQL技术方面的疑问和思考,并且结合实例来进行分析和解答,在这个过程中,既解决了技术方面的问题,也提升了开发者的认知和水平。

本文作为系列文章的开篇,我们将会讨论一些和PostgreSQL数据库系统的背景相关的问题。笔者认为,这是除了技术本身之外,涉及到一个软件系统的价值观和方法论的基本问题,其实对于我们理解它是比较重要的。

什么是PostgreSQL

PostgreSQL的官方网站(下图)是: www.postgresql.org

0S.png

网站已经对于PostgreSQL,说得很清楚明白了。

PostgreSQL:

The World’s Most Advanced Open Source Relational Database

这句话里面,有三个要点:

  • 关系数据库系统

主要指关系数据模型和SQL语言的支持。说明PG是一个什么类型的软件和系统。

  • 开源软件

PG确实是开源软件。但笔者认为,PG的强大,更在于它拥有一个成熟稳定的开源技术社区,能够保证其作为开源软件系统的长期平稳发展,这其实比软件开源本身还重要。

  • 最先进

如果考虑的是开源关系数据库系统,它确实也名副其实。这主要体现在其强大而稳定的性能,对SQL完善的支持,丰富的特性,可扩展性等等。从这些角度来看,它也一点都不逊色于像Oracle、MS SQL Server这类主流的商业软件。

再说说这个产品名称是怎么来的。笔者在另一篇文章里面也提到过,PostgersSQL其实是三个单词(Post-gres-SQL)构成的:

  • gres: Ingres的简化(后详)
  • Post: 后Ingres的意思,意为这个软件发展来自Ingres
  • SQL: 使用SQL技术和语言的关系型数据库系统

简单而言,PostgerSQL的意思就是“支持SQL的后ingres数据库系统”。这其实简单的体现了这个产品的一个发展历史和过程。

Ingres的全称是Iteractive Graphics REtrieval System(交互式图形检索系统) 是始于1970年代早期加州大学伯克利分校的一个研究项目,在这个项目的研究基础上产生了关系型数据模型的理论和软件雏形;Postgres是其在80年代的后继项目,增加了类型定义和完整描述数据关系的能力;实际上Postgres计划在1993年已经终止;但是在1994年,有人在其BSD许可的基础上,利用其原始代码进行了后续开发,主要是加入SQL语言解释器,让它更像是一个标准的SQL数据库软件,命名为Postgres95,并在1996年重新命名为PostgreSQL,并作为开源软件系统,基于互联网进行分发,初始分发的版本就是PostgreSQL 6.0,可以视为PostgreSQL软件的正式开端。

为什么要选择PostgreSQL

单纯的讨论这个问题,或者和其他软件进行技术细节方面的比较,其实也没有太大的意义。因为实际上,任何一个软件,只要有人使用,就可能有它的独特价值和应用场景。没有软件天生就好或者烂,好的软件都是逐渐发展和完善起来的,关键是这个改善的过程能否平稳持续,并且能够与时俱进。

当然,选择PG肯定也是有一些理由的,笔者这里想从借用一张Hasso Plattner Institut制作的关系型数据库家谱开始:

RDBMS_Genealogy_V6.jpg

从这个家谱,我们应该可以感觉到,在所有的关系数据库系统分分合合,纵横交错,新旧变换的历程来看,PostgreSQL的发展一直是比较顺利稳定的。同时,作为开源软件系统,它也非常重视开源社区的建设和管理,这些都奠定了它在技术发展上,具有一定稳定性、适应性和前瞻性。

笔者感觉,和其他系统相比,PostgerSQL在技术上是经典的开源和互联网软件开发体系,这些特点和优势在于:

  • 专注于互联网应用开发的支持,和传统关系数据库系统专注于企业应用开发构成差异化
  • 发展策略是小幅改进、快速迭代、重视生态,贴近和适应互联网应用开发技术的发展
  • 理论先行,作为学院派软件,PG具有前瞻性的系统架构,严谨规范的架构设计,合理的软件和特性的生命周期管理
  • 功能丰富,易于扩展,这些都是快速多变的互联网应用开发所需要的,例如对于多种数据类型、数组、JSON等的支持,pgcrypto、uuid扩展等等
  • 跨平台支持,由于开源和基于C语言开发的属性,PostgreSQL很早就规划了对于多个主流操作系统平台的支持
  • 标准的关系型数据库特性:关系数据模型、高效的存储和索引引擎、ACID、SQL语言、网络服务等等
  • SQL标准规范的积极支持,并扩展出很多强大和实用的使用方式,如CTE、Returning、filter等等
  • 软件系统的稳定性,虽然没有数据方面的支撑,但从笔者的使用体验还有社区口碑而言,这个系统是非常稳定和可靠的,而且对于运维的要求,并没有想象那么高,还是非常易用和健壮的

这里额外提供一个有趣的信息,PostgreSQL的产品logo(下图),就是一头大象。可能开发者认为大象可以很好的代表PosgreSQL的产品特性和形象,就是强大、稳健和可靠。

OIP-C.jpg

Postgres有什么缺点或者不足

从技术和宏观上来看,几乎没有。因为关系型数据库系统技术发展到现在,已经是非常成熟和稳定的了。市场上存活下来这么几家,基本上可用性都非常好,不会有什么无法被接受的功能缺失或者缺点,有一些差异,几乎完全取义于对于场景和适应程度,开发体系的匹配以及开发者喜好。

但如果要较真的话,这里也可以尝试例举几条:

  • XID

XID就是TranactionID(事务ID),PG使用XID来对事务进行标识和管理。但PG设计的XID是一个32位的无符号整数。它有容量限制(2^32,40亿)。如果数据库的修改操作过于频繁或者数据过于庞大,可能会溢出并导致数据错乱的情况。这时需要运维人员进行关注和维护。

  • 数据表分区

老版本的PG,没有提供完善的数据表分区方案,需要一些额外的设置和操作。但这一问题已经在最近几个版本的迭代中基本上得到了解决,现在的数据表分区,功能和易用性已经基本和Oracle无异。

  • 群集和高可用

这一点可能相比Oracle略微逊色。PostgreSQL没有原生的群集和高可用性技术,但它也提供了基于日志的数据复制机制。一些社区和第三方开发者在此基础上开发了扩展的高可用集群系统方案。

  • 技术资源

笔者觉得,Postgres的社区是做得非常好的。但可能有人会认为它作为开源软件体系,没法提供和商业软件同一级别的服务。还有,特别是在中国,PG的应用还没有像欧美那样广泛,相关的技术、人才的资源相对还是缺乏的。

小结

本文作为PostgreSQL技术问答的开篇,简单的讨论了PostgreSQL的发展背景,和作为技术和产品的特点,让读者在正式进入系列之前,对这个技术有一个相对客观合理的理解和认知。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

一个纯策略游戏——Goofspiel

发表于 2024-04-26

上周接了一个小单,根据一个用户的需求用C++做了个小游戏——Goofspiel。游戏逻辑很简单,两个用户A,B和一个奖励方C,用户方每一轮出一张牌比点数,大的就赢得奖励牌。一共13轮,结束后,谁手中奖励牌之和大,谁就胜利。注意,用户牌和奖励牌的大小都是1到13。 有想体验的可以去试试别人写的网页版:coppercod.games/game/gops/

Goofspiel game

图1. Goofspiel game 网页版

一个不存在最优解的纯策略游戏

首先,这是一个不存在最优解的纯策略游戏。因为它是双方博弈性质的,不同对手有不同的出牌偏好,这也决定了你的出牌倾向。换言之,不同对手出牌的概率分布是变化的。

常见的游戏策略

这类游戏的策略非常多,我们列举几个如下:

  • 随机策略:随机从手中取出一张牌出
  • 确定性策略:假设奖励牌为prize_card,那么我们出一张确定的牌;即下次奖励牌为prize_card,我们扔出那张确定的牌
  • 加x策略:奖励牌为prize_card,那么我们出的牌就是(prize_card+x)%13
  • 拿中值牌策略:采用+3的方式,侧重于4,5,6,7,8,9,10这7张中值牌。对于非目标牌,可以随机性出牌

策略的评估

不同策略有不同的应对方法,下面列举一些策略的应对方法:

  • 对于随机策略,即出牌服从[1,13]的均匀分布。我们只需要出与奖励牌一样的牌,就可以获得最后的点数期望最大。
  • 对于确定性策略,如果你知道对手采用+x策略,我们只需要采用+(x+1)策略。

我们的策略

考虑到高价值奖励牌往往并不容易获取,我们对拿中值牌策略进行了改进。它可以自适应的预测对手出牌大小通过考虑轮次、奖励牌情况、对手历 史出牌和我们的手牌。这个策略针对下面三类牌会做出不同的反应:

  • 较低价值牌: 对于1到3的奖励牌x,如果是1,我们出手上最小的纸牌;否则我们倾向于选择一个[x, x+2]的随机卡牌。
  • 中等价值牌 (Prize cards 4-10): 对于4到10的奖励牌x,对局一开始,我们会选择+3的策略。然后,我们会动态学习对手拿牌的gap (gap= 出牌-奖励牌),从而针对性的拿牌。
  • 高价值牌 (Prize cards 11-13): 对于11到13的奖励牌x,我们根据对局状况来选择小牌、随机牌或者大牌。具体来讲,如果我们手上大牌比对手大牌大,则我们拿下该牌;如果我们手上大牌和对手大牌一样大,我们随机选择中等价值牌或者低价值牌;如果我们手上大牌没有对手大牌大,我们将随机选择小牌或者第三个四分位牌。
    同时,为了限制对手学习能力,一旦我们得分超过45分,我们将随机选择手牌。

设计相关算法

  • 随机出牌检测算法:我们的算法会分析对手历史出牌,从而判断对手是否是随机出牌。具体来讲,如果对手是随机出牌,当轮数足够大时,对于任意奖励牌x,对手牌将遵循区间在1到13的均匀分布。由于大数定律可知,样本均值收敛于数学期望7。我们从而可以判断对手是否是随机算法。
  • 确定性出牌检测算法:对于一类确定性+x的算法,我们提出了针对性的检测算法。通过检测(对手出牌-奖励牌)%13是否为常数,我们可以确定对手是否是确定性出牌。
  • 自适应出牌算法:该算法会帮助我们的策略拿下更多的中等价值牌。具体来看,一开始我们会以+3的方式冷启动,如果对手与我们竞争中等价值牌,我们会记录对手出牌与奖励牌的gap(gap=对手出牌-奖励牌)。下次面对中等价值牌时,我们会通过 gap+1方式出更有竞争力的牌。
  • 动态丢牌算法:该算法会帮助我们的策略合理的丢弃高价值牌。具体来看,对于一张高价值牌(11,12 或 13),如果我们手中的大牌没有对手大,我们会随机选择丢弃小牌或者中等价值牌(防止对手不出大牌)。如果我们和对手手中的大牌一样大,对于比赛初,我们会随机丢弃中等价值牌或最小牌;对于比赛后期,我们会出最大牌(后期弃牌会非常被动)。

运行截图

下面,我们对上述算法的实现结果进行展示:

image.png

图2.游戏主界面

image.png

图3. 自适应出牌

image.png

图4. 确定性拿牌与动态丢牌

image.png

图5. 游戏结果展示

代码

具体代码可去我的github查看,github.com/guchengzhon…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…91011…399

开发者博客

3990 日志
1304 标签
RSS
© 2024 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%