图片处理解决方案Gimg发布了 作者: nbboy 时间: 2022-06-21 分类: 默认分类 评论 经过1个多月的开发,图片处理解决方案Gimg发布了。我对他的期望是能够方便的处理图片(废话),并且能够灵活的进行组合处理(强大的Lua功能),速度不会太慢,对后端存储的统一抽象(马上来了)。 可能有人会问市场上有这么多云图片处理方案,为什么还需要在开发一个,第一个是我们之前有这种需求,虽然说现在很多云的解决方案都已经覆盖了这些场景,但是总会有公司需要这些非云的方案,第二个是我对其中的一些设计比较感兴趣(闲的慌),所以才产生了Gimg。现在正在开发的是**多处理支持,和多存储方案的支持**,后面也会给出性能测试。他的Github地址是:https://github.com/x-debug/gimg
基础知识 作者: nbboy 时间: 2022-06-06 分类: 默认分类 评论 ### 基础知识有用么 最近在写一个服务的时候正好遇到这个问题,也正好看到陶老师的推送,就发表一下这方面的看法,就是对技术基础的看法。我自己还是比较重视基础的,但是学基础知识比较花费时间,所以进步一直很慢,可能也和遇到的场景有限有关。一直以来也有一个问题,就是对于web业务程序员来说,学习基础知识性价比是否高?首先这个问题的答案是肯定的,但是对于我不怎么遇到诸如高并发/高复杂度的场景,基础知识的重要性体现在哪里。作为已经参加工作好多年的程序员,不太可能会像读书的时候一样有大块时间用来学习,所以我们学习的知识应该尽量保证实用为主。不过技术革新太快了,还没弄明白云是怎么回事,诸如阿里云这样的公有云已经可以落地实施,现在中小公司基本也都在公有云上进行部署了,也还没弄明白虚拟化是怎么回事,已经有公司在用Docker进行开发和部署了,更不用说K8S套件这些了。各个云产品厂商纷纷吆喝自己的各种产品,搜索产品ES,消息队列等等,其实对于我们程序员来说这些完全属于黑盒,所以为了了解这些组件需要去学习。新的很对技术虽然错综复杂,看起来很新颖,但是其实用的都是老技术,这就是基础的重要性。陶老师已经在文章中举了很多例子,我们可以把学习这类知识理解为捷径,当然写出一个好的产品还需要创新(微创新)。 ### 学习哪些基础知识 基础知识也是挺多的,比如:1:数据结构,2:算法,3:计算机体系结构,4:操作系统,5:编译原理,6:计算机网络,7:数据库,8:软件工程。其实这些就是上学时候学过的,但是没学精,现在网上有很多知名大学的公开课和教材,国外的这方面书籍的确好了一大截。 ### 如何学习基础知识 学习计算机最好的方法还是去写代码和读代码,或者参与开源。 ### 推荐 https://www.taosdata.com/blog/2022/05/29/9853.html https://rango.swoole.com/archives/570
本地运行PySpark处理wasbs blobstorage数据 作者: nbboy 时间: 2022-06-05 分类: Python 评论 ### azure wasbs协议 wasbs文件协议是azure blobstorage,spark需要安装一些jar包才能支持该协议,并不是在自带的core中。这里简单记录一下安装过程和遇到的一些坑。 ### 安装Jar包 在安装好后的spark home目录的jars下放入jar包: azure-storage-8.6.6.jar azure-storage-blob-11.0.1.jar hadoop-azure-3.2.0.jar 在运行的时候报错说Jetty没有安装,于是索性下载了一些Jetty包,请注意版本,因为我用的是jdk8,所以下载的是低版本。 jetty-http-9.3.24.v20180605.jar jetty-server-9.3.24.v20180605.jar jetty-util-9.3.24.v20180605.jar jetty-util-ajax-9.3.24.v20180605.jar ### 连接Pyspark 接下来做的事让Pyspark找到Spark,接着再设置SparkSession。根据电脑的配置设置,如果Driver内存设置的太小,跑的过程中就会爆出内存不够的错误,这是我的配置: ```python import findspark findspark.init() print(findspark.find()) import pyspark from pyspark.sql import SparkSession #Create SparkSession spark = SparkSession.builder \ .appName("HQV Model") \ .config("spark.executor.memory", "4g") \ .config("spark.driver.memory", "4g")\ .config("spark.dirver.maxResultSize", "4g")\ .master("local[*]") \ .config("spark.executor.cores", "4") \ .config("spark.default.parallelism", "4") \ .getOrCreate() ```
《第四章.数据编码与演化》 作者: nbboy 时间: 2022-05-17 分类: 软件架构,软件工程,设计模式,Golang 评论 ### 数据编码用处 数据编码主要被用在数据传输和存储上,至于数据在内存中是不需要特定的数据编码的,用某种结构就可以表示(比如数组,链表,数,图等等),平时接触最多的可能就从数据库反射回来的数据模型对象。 ### 数据编码困境 数据编码即可以是通用的数据编码也可以是语言专用的数据编码,比如大家都知道的XML,JSON,PB是通用的数据编码,任何的语言基本都可以编码和解码。对于专有数据编码,比如Python Pickle文件和Java序列化等都属于这类,一般被用来存储的用途。 但是无论对于哪种数据编码,主要需要解决的问题是: - 保持双向兼容(前向和后向) - 快速的编码和解码 - 数据是否紧凑 - 最好对人类友好,即便于调试 以比较熟悉的JSON和PB进行描述和对比,JSON并没有保持双向兼容的能力,需要程序去自行处理。从性能上来说,相对于其他二进制编码,它无疑编解码也是比较慢的,而且因为是文本格式,所以数据也不具有太好的压缩特性。但是因此带来的好处是方便我们去调试,比如可以很方面的截获一段HTTP报文,我们完全不借助工具就可以阅读他的内容,这是其他二进制编码格式做不到的。所以正是这些因素,外网常用的接口都以Restful风格+JSON编码去传输。但是,PB正好相反,它可以预定义模式协议文件(pb文件),而且支持可选和必选,这样就为双向兼容提供了方便。PB编解码性能比JSON快了非常多,另外数据更加紧凑,不过由于以上特性,如果不借助工具,不方便调试。 以上描述的两种编码格式比较常见,也比较有代表性。不存在最好的和具备所有优点的编解码方式,需要我们根据具体场景去选择,比如目前比较好的实践方式是,走外网的接口通常用JSON去传输,内网的RPC服务用的更多的还是PB格式。 ### 数据流模式 在Web应用开发中,一般和数据库,消息队列,调用第三方服务打交道,书中总结抽象为数据流。 - 数据库也可以看成编解码的过程,把SQL的记录行组装成模型对象(不是领域对象)称为解码,把模型对象拼装成SQL语句执行的过程称为编码。消息队列和服务调用过程中更加少不了编解码。 - RPC和Restful在交互过程中对内容进行编解码 - 异步消息在投递到消息代理过程中会进行编解码 ### 总结 数据在存储和传输过程中需要进行编解码,在一些特殊的场景会自己定义一套编解码方式,目标是更好的性能和更具经凑的格式。当然当然在Web开发中,用的最多的还是JSON和PB,以后肯定会有更多的方案出现,但是都应该围绕上面提到的几个目标去设计。
Redis Hash原理 作者: nbboy 时间: 2022-02-27 分类: 默认分类,C 评论 > 分析的Redis版本基于6.0 ## Redis Hash #### 应用场景 哈希列表结构是一般在编程上非常重要的结构,因为它的查询性能接近O(1),所以是一种非常高效的数据结构。我们平时用的字典(dict)往往都是通过哈希列表去实现的,而redis中自己实现的hash结构也非常重要。在redis中所有的键值都是通过hash结构去管理的,比如我们键入命令set a hello时,其实在redis server用结构redisDb->dict来进行维护,其键和值都是redisObject: ```c typedef struct redisDb { dict *dict; /* The keyspace for this DB */ dict *expires; /* Timeout of keys with a timeout set */ ... } redisDb; ``` #### Hash结构 redis hash结构和普通的hash结构还是非常像的,但是因为它有一个渐进式hash过程(下面会讲),所以它其实有两个hash table。 ```c //字典采用链表处理冲突,而且在数据过多的时候会自动进行渐进式重新hash typedef struct dict { //一些函数指针 dictType *type; void *privdata; //rehash的oldhash和newhash dictht ht[2]; //rehash的进度(hash槽索引), 如果是-1, 则表示没有进行rehash, 否则就是在rehash long rehashidx; /* rehashing not in progress if rehashidx == -1 */ unsigned long iterators; /* number of iterators currently running */ } dict; typedef struct dictht { //指向hash槽的头部 dictEntry **table; //分配的hash槽总数量 unsigned long size; //计算hash槽的掩码值, 它等于size - 1 unsigned long sizemask; //在用的hash槽数量 unsigned long used; } dictht; //字典的每个元素 typedef struct dictEntry { //元素的健 void *key; //指向的值,或是指向一个对象,或者数值... union { void *val; uint64_t u64; int64_t s64; double d; } v; //采用链表的方式,next指向hash槽内的下个元素 struct dictEntry *next; } dictEntry; ``` 比如size=6的hash表按照字母顺序进行编码(比如a:1,b:2,c:3,d:4,e:5,f:6,g:1...),并且插入到hash列表中后的图示如下:  #### 键冲突处理 我们知道所有的hash列表都会冲突,冲突是由于key被hash到一个bucket(有些资料里叫hash槽)里引起的。Redis采用如下的技术处理键的冲突: - 选择好的Hash函数,尽量让键分布均匀 - 用拉链的方式处理冲突,即使键值过载,也会被分散到bucket内的链表里 - 设定一个阈值,如果元素过载,则会进行自动扩容 散列表的处理冲突方式一般有两种,开放定址和拉链法,而Redis采用的是拉链法,实现上用链表来管理冲突的键值对。Redis在Hash函数的选取上则用的是SipHash,这种算法有较强的安全性,可以用来防御“Hash-flooding”。 #### 扩容/缩容 当随着元素的增加,如果Hash列表不进行扩张,则必然会增加单个Bucket的元素数量,而这会导致命中该Bucket的检索操作退化到查询列表的操作,而这是不能被接受的,所以Redis设计了自动扩容的机制。 扩容是在添加操作完成的,当元素使用比例过了设定的阈值,则进行自动扩容: ```c //超过阈值,开始进行hash扩展,扩展长度为两倍,里面会进行微调整 if (d->ht[0].used >= d->ht[0].size && (dict_can_resize || d->ht[0].used/d->ht[0].size > dict_force_resize_ratio)) { return dictExpand(d, d->ht[0].used*2); } ``` Redis扩容本身并不复杂,它会把散列的空间扩大到现有元素的2倍左右大小。相比扩容,缩容采用了更加主动的方式,采用定时器进行周期性的空间回收,具体操作在tryResizeHashTables函数,收缩的条件为空间利用率下降为10%,就会进行一次收缩,收缩过后的大小刚好大于原有键值对数量的2的n次。([dictResize](https://github.com/redis/redis/blob/6.0/src/dict.c#L135 "dictResize")) ```c //低于10%的时候,会进行缩容 int htNeedsResize(dict *dict) { long long size, used; size = dictSlots(dict); used = dictSize(dict); //use/size < 0.10 return (size > DICT_HT_INITIAL_SIZE && (used*100/size < HASHTABLE_MIN_FILL)); } ``` #### 渐进式Rehash 作为hash列表,已有的功能都是完整的,但是作为高性能的Redis服务器如果只是容许暂停服务进行rehash,那将是不可承受的。Redis在设计上采用了渐进式的结构,也就是在rehash阶段,会有两个散列表存在,一个用来存目前用的健值对ht1,另外一个则是新扩张后的ht2。在这个阶段两个ht都需要去查找和更新,当完成这个阶段只需要考虑ht1。 可以用一个例子来图示一下,假设为了方便演示,resize比例为1的时候就进行扩张,初始化hash大小为2个Bucket,插入3个键值对后的hash状态如下:  这时候,其实hash列表里的键值对已经达到扩容的零界点,当请求插入l=33的时候,根据used/size=4/2=2>1(dict_force_resize_ratio ),所以会分配一个2倍空间(2*used)大小给新的ht1,并且启动**渐进式hash迁移**。而在hash迁移期间,所有的插入请求的键值对都会放到新的ht1。  当我们请求删除e为健的键值对时,其实它会进行两步操作(其实如果一旦进入hash迁移,则对于查找/添加/替换都会有下面的第一步): 1. 依次获取ht0的一个bucket,并且迁移到ht1中 2. 在ht0和ht1中查找是否有健,如果找到的话进行删除  *图画的不是很好,表达的是2个键值对迁移过来后,其中一个被删除(画叉的是被删除,画加号的是被正常迁移过来,顺序是先迁移,后删除)* 这时候如果来个检索操作,比如检索b为健的值是多少,同样检索操作也需要执行一次迁移操作,也即: 1. 依次获取ht0的一个bucket,并且迁移到ht1中 2. 在ht0和ht1中查找是否有健,如果找到的话返回 对于这里的例子,本次操作会迁移Bucket2,操作后会使ht0没有键值对(used==0)。而当ht0为空时候,ht1被作为新的ht0(ht1指针给ht0),ht1清空作为下一次迁移用,本轮迁移标记rehashidx置空。  需要说明的是如果在迁移阶段进行检索,并且第一步之后迁移也没有完成,则查找的时候会对h0和h1两个hash表都进行检索,因为键值有可能在h0也有可能在h1。 最后再补充一点,渐进式迁移不光光在查找/添加/替换等操作里有,而且在定时任务里也会给予一定的时间(1ms)进行迁移。 ### 参考 《算法》散列表部分 《数据结构与算法之美》散列表部分