前端技术
HTML
CSS
Javascript
前端框架和UI库
VUE
ReactJS
AngularJS
JQuery
NodeJS
JSON
Element-UI
Bootstrap
Material UI
服务端和客户端
Java
Python
PHP
Golang
Scala
Kotlin
Groovy
Ruby
Lua
.net
c#
c++
后端WEB和工程框架
SpringBoot
SpringCloud
Struts2
MyBatis
Hibernate
Tornado
Beego
Go-Spring
Go Gin
Go Iris
Dubbo
HessianRPC
Maven
Gradle
数据库
MySQL
Oracle
Mongo
中间件与web容器
Redis
MemCache
Etcd
Cassandra
Kafka
RabbitMQ
RocketMQ
ActiveMQ
Nacos
Consul
Tomcat
Nginx
Netty
大数据技术
Hive
Impala
ClickHouse
DorisDB
Greenplum
PostgreSQL
HBase
Kylin
Hadoop
Apache Pig
ZooKeeper
SeaTunnel
Sqoop
Datax
Flink
Spark
Mahout
数据搜索与日志
ElasticSearch
Apache Lucene
Apache Solr
Kibana
Logstash
数据可视化与OLAP
Apache Atlas
Superset
Saiku
Tesseract
系统与容器
Linux
Shell
Docker
Kubernetes
[MPP架构并行数据处理技术 ]的搜索结果
这里是文章列表。热门标签的颜色随机变换,标签颜色没有特殊含义。
点击某个标签可搜索标签相关的文章。
点击某个标签可搜索标签相关的文章。
Hadoop
...人抓狂!作为一个对大数据技术充满热情的技术宅男(或者宅女),这种问题简直就像一道数学题里的“未知数”一样困扰着我。今天,我就想跟大家聊聊这个话题,希望能找到一些解决办法。 一、背景介绍 HDFS为什么重要? 首先,让我们简单回顾一下HDFS是什么。HDFS(Hadoop分布式文件系统)就像是Hadoop这个大家族里的“顶梁柱”之一,它专门用来管理海量的数据,就像一个超级大的仓库,能把成千上万的数据文件整整齐齐地存放在不同的电脑上,还能保证它们既安全又容易取用。简单来说,就是把一个大文件分成很多小块,然后把这些小块分散存储在不同的服务器上。这么做的好处嘛,简直太明显了!就算哪台机器突然“罢工”了,数据也能稳稳地保住,完全不会丢。而且呢,还能同时对这些数据进行处理,效率杠杠的! 但是,任何技术都有它的局限性。HDFS虽然功能强大,但在实际应用中也可能会遇到各种问题,比如读取速度慢。这可能是由于网络延迟、磁盘I/O瓶颈或者其他因素造成的。那么,具体有哪些原因会导致HDFS读取速度变慢呢?接下来,我们就来一一分析。 二、可能的原因及初步排查 1. 网络延迟过高 想象一下,你正在家里看电影,突然发现画面卡顿了,这是因为你的网络连接出了问题。同样地,在HDFS中,如果网络延迟过高,也会导致读取速度变慢。比如说,假如你的数据节点散落在天南海北的各种数据中心里,那数据跑来跑去就得花更多时间,就像你在城市两端都有家一样,来回折腾肯定比在同一个小区里串门费劲得多。 示例代码: java Configuration conf = new Configuration(); FileSystem fs = FileSystem.get(conf); Path filePath = new Path("/user/hadoop/input/file.txt"); FSDataInputStream in = null; try { in = fs.open(filePath); byte[] buffer = new byte[1024]; int bytesRead = in.read(buffer); while (bytesRead != -1) { bytesRead = in.read(buffer); } } catch (IOException e) { e.printStackTrace(); } finally { if (in != null) { try { in.close(); } catch (IOException e) { e.printStackTrace(); } } } 这段代码展示了如何从HDFS中读取文件。如果你发现每次执行这段代码时都需要花费很长时间,那么很可能是网络延迟的问题。 2. 数据本地性不足 还记得小时候玩过的接力赛吗?如果接力棒总是从一个人传到另一个人再传回来,效率肯定不高。这就跟生活中的事儿一样啊,在HDFS里头,要是数据没分配到离客户端最近的那个数据节点上,那不是干等着嘛,多浪费时间呀! 解决方案: 可以通过调整副本策略来改善数据本地性。比如说,默认设置下,HDFS会把文件的备份分散存到集群里的不同机器上。不过呢,如果你想让这个过程变得更高效或者更适合自己的需求,完全可以去调整那个叫dfs.replication的参数! xml dfs.replication 3 3. 磁盘I/O瓶颈 磁盘读写速度是影响HDFS性能的一个重要因素。要是你的服务器用的是那些老掉牙的机械硬盘,那读文件的速度肯定就慢得像乌龟爬了。 实验验证: 为了测试磁盘I/O的影响,可以尝试将一部分数据迁移到SSD上进行对比实验。好啦,想象一下,你手头有一堆日志文件要对付。先把它们丢到普通的老硬盘(HDD)里待着,然后又挪到固态硬盘(SSD)上,看看读取速度变了多少。是不是感觉像在玩拼图游戏,只不过这次是在折腾文件呢? 三、进阶优化技巧 经过前面的分析,我们可以得出结论:要提高HDFS的读取速度,不仅仅需要关注硬件层面的问题,还需要从软件配置上下功夫。以下是一些更高级别的优化建议: 1. 增加带宽 带宽就像是高速公路的车道数量,车道越多,车辆通行就越顺畅。对于HDFS来说,增加带宽意味着可以同时传输更多的数据块。 实际操作: 联系你的网络管理员,询问是否有可能升级现有的网络基础设施,比如更换更快的交换机或者部署新的光纤线路。 2. 调整副本策略 默认情况下,HDFS会将每个文件的三个副本均匀分布在整个集群中。然而,在某些特殊场景下,这种做法并不一定是最优解。比如说,你家APP平时就爱扎堆在那几个服务器节点上干活儿,那就可以把副本都放一块儿,这样它们串门聊天、传文件啥的就方便多了,也不用跑太远浪费时间啦! 配置修改: xml dfs.block.local-path-access.enabled true 3. 使用缓存机制 缓存就像冰箱里的剩饭,拿出来就能直接吃,不用重新加热。HDFS也有类似的机制,叫做“DataNode Cache”。打开这个功能之后啊,那些经常用到的数据就会被暂时存到内存里,这样下次再用的时候就嗖的一下快多了! 启用步骤: bash hadoop dfsadmin -setSpaceQuota 100g /cachedir hadoop dfs -cache /inputfile /cachedir 四、总结与展望 通过今天的讨论,我相信大家都对HDFS读取速度慢的原因有了更深的理解。其实,无论是网络延迟、数据本地性还是磁盘I/O瓶颈,都不是不可克服的障碍。其实吧,只要咱们肯花点心思去琢磨、去试试,肯定能找出个适合自己情况的办法。 最后,我想说的是,作为一名技术人员,我们应该始终保持好奇心和探索精神。不要害怕失败,也不要急于求成,因为每一次挫折都是一次成长的机会。希望这篇文章能给大家带来启发,让我们一起努力,让Hadoop变得更加高效可靠吧! --- 以上就是我对“HDFS读取速度慢”的全部看法和建议。如果你还有其他想法或者遇到类似的问题,请随时留言交流。咱们共同进步,一起探索大数据世界的奥秘!
2025-05-04 16:24:39
103
月影清风
Netty
...,随着云计算和微服务架构的普及,越来越多的企业开始采用Netty作为其后端服务的核心框架。特别是在金融行业,由于对系统稳定性和安全性要求极高,Netty因其卓越的性能和灵活的扩展性备受青睐。例如,某大型银行的支付清算系统近期完成了全面升级,采用了基于Netty的微服务架构,实现了每秒百万级的交易处理能力,同时大幅降低了系统延迟和资源消耗。这一成功案例不仅验证了Netty在高并发场景下的强大适应性,也为其他金融机构提供了宝贵的经验。 此外,在物联网领域,Netty的应用同样值得关注。随着智能家居设备数量的激增,如何高效处理海量设备的实时通信成为一大挑战。Netty凭借其轻量级、非阻塞的特性,成为了许多物联网平台的首选解决方案。例如,一家领先的智能家居公司通过引入Netty,成功构建了全球化的设备管理平台,支持数千万台设备的同时在线。该平台不仅实现了设备间的数据同步,还通过心跳检测和长连接复用等技术,确保了通信的稳定性和可靠性。 从技术发展的角度来看,Netty的未来潜力依然巨大。随着Rust等新兴编程语言逐渐进入主流视野,未来的Netty版本可能会结合多语言特性,进一步提升系统的兼容性和性能。同时,随着量子计算技术的发展,Netty的研究者们也在积极探索如何将这一前沿技术融入框架中,以应对未来更大规模的分布式计算需求。这些趋势表明,Netty不仅在当下具有重要意义,还将继续引领未来的网络编程潮流。
2025-03-19 16:22:40
79
红尘漫步
Mahout
名词 , 实时流数据分析。 解释 , 实时流数据分析是在数据生成的同时进行的数据分析,它能即时处理大量连续涌入的数据流,提供实时洞察。这种分析方法在金融、互联网、物联网等行业尤为重要,能够帮助企业快速响应市场变化,做出实时决策。 名词 , 分布式计算。 解释 , 分布式计算是一种将计算任务分解并分配给多个计算节点进行并行处理的技术。在文中,分布式计算是Mahout运行的基础,通过Hadoop等框架,Mahout能够在多台计算机之间分配和执行任务,提高数据处理的效率和规模,适用于处理大规模数据集。 名词 , 机器学习。 解释 , 机器学习是一门研究如何使计算机自动从数据中学习规律,无需明确编程的学科。在文章中,机器学习通过Mahout实现,用于处理和分析实时数据流,提取有价值的信息,支持实时决策制定。它包括监督学习、无监督学习、强化学习等多种方法,能自动优化模型参数,提高分析的准确性和效率。
2024-09-06 16:26:39
59
月影清风
转载文章
...a JMS进行监听和处理后,进一步了解消息队列技术在现代企业级应用中的实践与发展显得尤为重要。近期,Oracle发布了最新版本的数据库产品,其中对AQ组件进行了多项优化升级,不仅提升了消息处理效率,还增强了与云环境和其他消息服务的集成能力。 2022年,Oracle官方博客分享了一篇题为《Oracle AQ的新特性及其在微服务架构中的应用》的文章,详细解读了Oracle 19C及更高版本中AQ的改进之处,如支持JSON格式的消息负载、更灵活的多租户管理和跨数据库的分布式队列功能等。这些新特性使得AQ能够更好地适应当前流行的微服务架构,实现不同服务间高效可靠的数据传输与同步。 此外,在开源社区层面,Apache ActiveMQ Artemis作为一款广泛采用的消息中间件,也在持续演进以满足不断变化的企业需求。其与Oracle AQ的兼容性有所提升,用户现在可以在多种场景下根据实际业务需求选择适合的消息队列解决方案。 同时,对于Java开发者而言,《Java Message Service (JMS)实战》一书提供了大量关于利用JMS进行消息传递的实战案例和最佳实践,有助于读者在实际项目中更加熟练地运用JMS与Oracle AQ结合,构建高性能、高可用的消息驱动系统。 综上所述,无论是紧跟Oracle AQ的最新发展动态,还是探究开源替代方案与相关技术书籍的学习,都将帮助开发者更好地掌握消息队列技术,并将其应用于实际工作中,以提升系统的整体性能与稳定性。
2023-12-17 14:22:22
138
转载
Ruby
...人工智能领域,多线程处理的需求愈发旺盛。例如,亚马逊AWS最近推出了一项名为“Firecracker”的微虚拟化技术,旨在为无服务器计算提供更高的性能和安全性。这项技术利用轻量级虚拟化容器来运行多个任务,极大地提高了资源利用率。然而,这种高度并发的环境也带来了新的挑战,比如如何确保不同任务之间的数据隔离性和一致性。 在国内,阿里巴巴集团也在积极布局并发编程相关的技术研究。阿里云推出了基于Go语言的高性能微服务框架“MOSN”,该框架支持大规模分布式系统的构建,特别适合处理高并发场景下的请求分发和负载均衡。MOSN的设计理念强调模块化和可扩展性,使得开发者能够轻松应对复杂的业务逻辑。不过,随着越来越多的企业采用类似的架构,如何有效管理线程池大小、避免死锁等问题成为了新的关注焦点。 此外,近期一篇发表在《ACM Transactions on Programming Languages and Systems》上的论文引起了广泛关注。这篇论文探讨了现代编程语言在并发模型设计上的差异,并提出了一种新型的“乐观并发控制”算法。该算法通过预测线程间的冲突概率,动态调整同步策略,从而在一定程度上减少了锁的使用频率。这一方法不仅提升了程序的执行效率,还降低了开发者的维护成本。 从哲学角度来看,无论是技术层面还是理论层面,人类对于并发编程的追求始终未曾停歇。正如古希腊哲学家赫拉克利特所言:“人不能两次踏进同一条河流。”同样,在并发编程的世界里,每一次尝试都是一次全新的探索,而每一次成功都离不开对失败教训的深刻反思。未来,随着量子计算等前沿科技的发展,我们或许将迎来一场关于并发编程范式的革命,而这无疑将为软件工程领域带来前所未有的机遇与挑战。
2025-04-25 16:14:17
32
凌波微步
Kafka
近期,随着云原生技术的快速发展,Kafka作为分布式消息中间件的应用场景愈发广泛。特别是在微服务架构日益普及的背景下,Kafka因其高吞吐量、低延迟的特点,成为了企业级数据流处理的首选方案。然而,这也带来了新的挑战。例如,国内某大型电商企业在双十一促销活动中,由于订单峰值激增,其基于Kafka构建的实时交易系统一度面临消息堆积的问题。经过紧急排查,发现主要是由于分区数量不足导致的负载不均。为此,该企业迅速调整了分区策略,并优化了消息生产和消费逻辑,最终顺利应对了高峰流量。 与此同时,国外科技巨头也对Kafka进行了持续改进。近日,Confluent公司宣布推出Kafka 3.6版本,该版本引入了多项新特性,包括增强型事务API、更高效的压缩算法以及对多租户环境的支持。这些更新旨在帮助企业更好地满足复杂业务场景的需求,同时也反映了Kafka社区对于技术创新的不懈追求。 此外,关于Kafka与ZooKeeper的关系,业界普遍关注其未来的演进方向。尽管Confluent正在推动KRaft(Kafka Raft-based Controller)项目,试图完全摆脱ZooKeeper的依赖,但在短期内,ZooKeeper仍将在许多传统部署环境中占据主导地位。因此,对于正在使用Kafka的企业而言,如何平衡现有基础设施与新技术之间的过渡,成为了一个值得深思的问题。 从长远来看,Kafka的成功离不开开源社区的支持。正如Apache软件基金会所倡导的理念,“开放、协作、共享”始终是推动技术创新的核心动力。在未来,随着更多企业和开发者加入到Kafka生态中,我们有理由相信,这一技术将继续保持旺盛的生命力,并在更多领域发挥重要作用。
2025-04-05 15:38:52
95
彩虹之上
Golang
...,随着云计算和微服务架构的普及,Golang因其高效性和简洁性逐渐成为构建云原生应用的首选语言。特别是在Kubernetes和Docker等技术的推动下,Golang的生态系统愈发繁荣。最近,一项关于全球开发者调查的研究显示,Golang已经成为增长最快的编程语言之一。这不仅反映了开发者社区对其性能的认可,也表明企业在选择技术栈时更加注重效率和可维护性。 例如,Netflix最近宣布将其内部工具和服务迁移到Golang上,以应对日益复杂的流媒体需求。Netflix的技术团队表示,Golang的轻量级协程和高效的垃圾回收机制显著提升了系统的响应速度和稳定性。此外,Golang的跨平台编译能力也让Netflix能够更轻松地部署和管理在全球范围内的服务器集群。 与此同时,国内的科技巨头也在积极拥抱Golang。阿里巴巴集团旗下的蚂蚁金服和阿里云相继推出了基于Golang的开源项目,如Dubbo-go和PolarDB-X。这些项目不仅展示了Golang在企业级应用中的潜力,也为其他开发者提供了丰富的学习资源。特别是在金融和电商领域,Golang凭借其高性能和低延迟的优势,正在逐步取代Java等传统语言。 值得一提的是,Golang的快速发展也引发了学术界的高度关注。近期,一篇发表在《ACM Computing Surveys》上的论文指出,Golang的设计哲学与现代软件工程的最佳实践高度契合。论文作者认为,Golang的成功不仅仅在于其技术特性,还在于它重新定义了开发者的工作方式,使其更加专注于业务逻辑而非底层实现细节。 展望未来,随着5G、物联网和人工智能等新技术的兴起,Golang有望在更多领域大放异彩。无论是边缘计算、大数据处理还是实时数据分析,Golang都展现出了巨大的潜力。正如Google Go团队负责人Robert Griesemer所说:“Golang的目标始终是让开发者能够更快、更好地完成工作。”这种理念无疑将继续引领技术发展的潮流。
2025-04-23 15:46:59
39
桃李春风一杯酒
转载文章
...深入理解了AWS高级技术讲师黄涛对于AWS服务、技术方向选择、学习资源以及认证体系的详细介绍后,我们可以进一步关注AWS在实际应用中的最新动态和发展趋势。近期,AWS在全球范围内持续扩张其基础设施,并发布了一系列创新服务和技术更新。 例如,在2022年,AWS宣布将在多个国家增设新区域和可用区,以满足全球用户对低延迟和数据主权的需求。同时,AWS也推出了Lambda SnapStart功能,显著提升了无服务器计算服务Lambda的启动速度,增强了其在实时应用和大规模并行处理场景下的表现。 此外,随着企业数字化转型的加速,AWS在安全合规方面加大投入,强化了IAM角色权限管理机制,并新增多项数据加密与隐私保护功能,紧密贴合最小授权原则。值得一提的是,AWS最近发布的Well-Architected Tool改版升级,提供了更详尽的最佳实践指导和自动化评估工具,有助于架构师们在设计系统时遵循云设计架构原则,确保高可用性、安全性和成本效益。 对于有志于取得AWS认证的专业人士,可以关注AWS官方培训课程及社区活动的最新信息,如定期举行的AWS在线研讨会、直播讲座等,这些活动将及时分享AWS服务的最新特性、最佳实践案例以及考试策略。同时,通过阅读AWS白皮书和行业分析报告,了解AWS在不同行业解决方案中的实际运用,结合理论与实战经验,为备考助理级或专家级架构师认证做好充分准备。
2023-11-29 22:08:40
270
转载
转载文章
在深入理解SQL数据库操作的诸多要素之后,我们可以关注近期关于数据库优化和安全性的前沿动态。近日,MySQL官方发布了8.0.28版本,该版本强化了对窗口函数的支持,并提升了索引条件推送的性能,使得复杂查询得以更高效地执行。同时,针对多表查询优化策略,许多数据库专家和社区成员正在探讨如何借助物化视图、分区表等高级功能进一步提升查询速度。 此外,随着数据安全问题日益凸显,触发器在保障数据一致性与合规性方面的作用受到更多重视。例如,在金融交易系统中,通过精心设计的触发器可实现对关键业务数据的实时审计追踪。而在数据同步场景下,触发器结合流处理技术(如Debezium)实现实时增量数据同步,已被广泛应用在微服务架构中。 另一方面,存储过程的安全性与性能优化也成为了热门话题。有研究指出,通过合理设计和使用参数化存储过程,不仅可以减少SQL注入风险,还能有效提高数据库系统的整体性能。尤其在大数据环境下,企业开始探索利用存储过程进行批量化数据清洗和预处理,以减轻服务器负载并确保数据质量。 最后,针对数据库隐私保护,各大云服务商正积极引入同态加密、动态数据屏蔽等前沿技术,这些技术在不影响查询性能的前提下,增强了数据在存储及传输过程中的安全性,为用户提供了更为全面的数据安全保障。对于SQL开发者而言,紧跟这些技术趋势和实践案例,无疑将有助于更好地应对未来数据库管理和查询优化的挑战。
2023-04-26 19:09:16
83
转载
转载文章
...阿里云服务与深度学习技术进行证件照生成的创新研究后,读者可能对图像分割算法在实际应用中的最新进展以及阿里云在人工智能领域的持续探索感兴趣。以下是一则延伸阅读内容: 近期,阿里云发布了全新的图像处理解决方案,该方案集成了最新的深度学习模型和优化算法,可广泛应用于各类证件照、商品图片等场景的智能生成与编辑。其中,研究人员借鉴了类似SeedNet网络的多任务学习机制,并结合时空注意力机制进一步提升了图像分割的精度和效率。 此外,全球范围内的人工智能研究也在图像分割领域取得了突破性进展。例如,微软亚洲研究院今年公开了一项名为“HRNet”的高分辨率网络架构,它在医疗影像、自动驾驶等多个领域保持了顶级的图像分割性能。这一成果证明了通过保留高分辨率特征图并进行跨层次的信息融合,可以有效解决复杂背景下的精细化分割难题。 同时,阿里云不仅在技术研发上持续发力,还积极助力各行业实现数字化转型,如为教育、金融等行业提供了基于云服务的一站式证件照生成平台,用户只需上传原始照片即可快速获得满足各类规范要求的证件照,大大简化了传统流程,提高了工作效率。 综上所述,随着深度学习技术的不断迭代和云服务生态的完善,图像分割在证件照生成领域的应用正逐步走向成熟,并展现出巨大的市场潜力和社会价值。而作为行业领导者之一的阿里云,将持续引领技术创新,推动相关应用场景的落地与发展。
2023-07-11 23:36:51
131
转载
转载文章
...。据比达咨询市场分析数据显示,2016年中国第三方餐饮外卖市场格局中,饿了么位居第一,市场份额为34.6%,美团外卖(33.6%)、百度外卖(18.5%)紧随其后,在“白领市场”、“社区市场”、“校园市场”的细分领域中,饿了么均占据榜首位置。截至2016年12月,饿了么业务覆盖1400多个城市,用户超过1亿,各地加盟餐厅超过100万家,日订单量突破900万,旗下“蜂鸟配送”日配送单量超过450万。 在 “独角兽”的成长道路上,饿了么面对人工成本高制约业务快速扩张、人工派单速度慢导致高峰期积压订单严重、人工派单随机性强引起订单配送时效性差等现实问题,而阿里云通过智能派单系统,基于海量历史订单数据、餐厅数据、骑手数据、用户数据等信息实现智能派单,逐步替代调度员的大部分工作。智能派单系统整体全面上线后将释放90%以上人工派单的人力,每年节省人力支出预计超过亿元。 饿了么的IT系统架构伴随业务量飙升,进行了三次重大升级。 1)起步期(2009至2013年):饿了么由上海交通大学创始团队起家,发展至35人规模,日订单量维持在十万量级,由“IDC+Python”技术组合支撑业务运营,但面临Python人才难觅等困扰。 2)成长期(2014年至2015年):14年8至9月短短2个月内日均订单量增长10倍,从10万迅猛飙升至100万,业务规模主攻全国200个城市,原有IT系统架构压力极大,依靠人肉运维举步维艰,故障波动影响业务,创始人与核心技术团队坚守机房运维一线,才勉强扛住100万量级业务订单。开始借鉴阿里淘宝架构模式,人员团队也涨至500人,技术生态从Python扩展至“Java+Python”开发体系,从“人肉”支撑百万订单运营到自动化运维,并筹备同城异地容灾体系。 3)规模期(2015年至2017年):2015年7至8月,日均订单量从200万翻倍,以往积压的问题都暴露出来,技术架构面临大考验,坚定了架构上云的方案,团队扩展至1000人,架构要承载数百万量级业务时,出现峰值成本、灾备切换、IDC远程运维等种种挑战,全面战略转型采用“IDC+云计算”的混合云架构。在2016年12月25日圣诞节日订单量迎来前所未有的900万单,因此在技术架构上探索多活部署等创新性研发。 为什么选择架构转型上云?据饿了么CTO张雪峰先生所说,技术架构从IDC经典模式发展至混合云模式,主要原因是三个关键因素让管理层下定决心上云: 1) 脉冲计算:从技术架构配套业务发展分析,网络订餐业务具有明显的“脉冲计算”特征,在每日上午10:00至13:00、晚间16:00至19:00业务高峰值出现,而其他时间则业务量很低,暑假是业务高峰季,2016年5.17大促,饿了么第一次做“秒杀”,一秒订单15000笔,巨大的波峰波谷计算差异,引发了自建数据中心容量不可调和的两难处境,如果大规模投入服务器满足6小时的高峰业务量,则其余18个小时的业务低谷计算资源闲置,若满足平均业务量,则无法跟上业务快速发展节奏,落后于竞争对手;搞电商大促时,计算资源投入巨大,大促之后计算峰值下降,采用自建机房利用率仅10%,所以技术团队摸索出用云计算扛营销大促峰值的新模式,采用混合云架构满足 “潮汐业务”峰值计算,阿里云海量云计算资源弹性随需满足巨大的脉冲计算力缺口,这与每年“双11” 淘宝引入阿里云形成全球最大混合云架构具有异曲同工的创新价值。 2) 数据量爆炸:伴随饿了么近五年业务量呈几何级数的爆发式发展,数据量增速更加令人吃惊,是业务量增速的5倍,每日增量数据接近100TB,2015年短短2个月内业务量增长10倍,数据量增长了50倍,上海主生产机房不堪重负。30GB的DDoS攻击对业务系统造成较大风险,上云成为承载大数据、抗网络攻击的好方法。 3) 高可用性挑战:众所周知,IDC自建系统运维要承担从底层硬件到上层应用的“全栈运维”运营能力与维修能力,当2015年夏天上海数据中心故障发生,主核心交换机宕机时,备核心交换机Bug同时被触发,从事故发生到硬件厂商携维修设备打车赶往现场维修的整个过程中,饥饿的消费者无法订餐吃饭,技术团队第一次经历业务中断而束手无策,才下定决心大笔投入混合云灾备的建设,“吃一堑,长一智”,持续向淘宝学习电商云生产与灾备架构,以自动化运维替代人肉运维,从灾备向多活演进,成为饿了么企业架构转型的必经之路。 4) 大数据精益运营:不论网络打车还是网络订餐,共享服务平台脱颖而出的关键成功要素是智能调度算法,以大数据训练算法提升调度效率,饿了么在高峰时段内让百万“骑士”(送餐快递员)完成更多订单是算法持续优化的目标,而这背后隐藏着诸多复杂因素,包括考虑餐厅、骑士、消费者三者的实时动态位置关系,把新订单插入现有“骑士”的行进路线中,估计每家餐厅出餐时间,每个骑手的行进速度、道路熟悉程度各不相同,新老消费者获客成本、高价低价订单的优先级皆不相同。种种考量因素合并到一起,对于人类调度员来说,每天中午和晚上的高峰都是巨大的挑战。以上海商城路配送站为例,一个调度员每6秒钟就要调度1单,他需要考虑骑手已有订单量、路线熟悉度等。因此可以说,这份工作已经完全不适合人类。但对人工智能而言,阿里云ET则非常擅长处理这类超复杂、大规模、实时性要求高的“非人”问题。 饿了么是中国最大的在线外卖和即时配送平台,日订单量900万单、180万骑手、100万家餐饮店,既是史无前例的计算存储挑战,又是人无我有的战略发展机遇。饿了么携手阿里云人工智能团队,通过海量数据训练优化全球最大实时智能调度系统。在基础架构层,云计算解决弹性支撑业务量波动的基础生存问题,在数据智能层,利用大数据训练核心调度算法、提升餐饮店的商业价值,才是业务决胜的“技术神器”。 在针对大数据资源的“专家+机器”运营分析中,不断发现新的特征: 1) 区域差异性:饿了么与阿里云联合研发小组测试中发现有2个配送站点出现严重超时问题。后来才知道:2个站点均在成都,当地人民喜欢早、中餐一起吃,高峰从11点就开始了。习惯了北上广节奏的ET到成都就懵了。据阿里云人工智能专家闵万里分析:“不存在一套通用的算法可以适配所有站点,所以我们需要让ET自己学习或者向人类运营专家请教当地的风土人情、饮食习惯”。除此之外,饿了么覆盖的餐厅不仅有高大上的连锁店,还有大街小巷的各类难以琢磨的特色小吃,难度是其他智能调度业务的数倍。 2) 复杂路径规划:吃一口热饭有多难?送餐路径规划比驾车出行路径规划难度更高,要考虑“骑士”地图熟悉程度、天气状况、拼单效率、送餐顺序、时间对客户满意度影响、送达写字楼电梯等待时间等各种实际情况,究竟ET是如何实现智能派单并确保效率最优的呢?简单来说,ET会将配送站新接订单插入到每个骑手已有的任务中,重新规划一轮最短配送路径,对比哪个骑手新增时间最短。为了能够准确预估新增时间,ET需要知道全国100万家餐厅的出餐速度、超过180万骑手各自的骑行速度、每个顾客坐电梯下楼取餐的时间。一般来说,餐厅出餐等待时间占到了整个送餐时间的三分之一。ET要想提高骑手效率,必须准确预估出餐时间以减少骑手等待,但又不能让餐等人,最后饭凉了。饿了么旗下蜂鸟配送“准时达”服务单均配送时长缩短至30分钟以内。 3) 天气特殊影响:天气等环境因素对送餐响应时间影响显著,要想计算骑手的送餐路程时间,ET需要知道每个骑手在不同区域、不同天气下的送餐速度。如果北京雾霾,ET能看见吗?双方研发团队为ET内置了恶劣天气的算法模型。通常情况下,每逢恶劣天气,外卖订单将出现大涨,对应的餐厅出餐速度和骑手骑行速度都将受到影响,这些ET都会考虑在内。如果顾客在下雪天点个火锅呢?ET也知道,将自动识别其为大单,锁定某一个骑手专门完成配送。 4) 餐饮营销顾问:饿了么整体业务涉及C端(消费者)、B端(餐饮商户)、D端(物流配送)、BD端(地推营销),以往区域业务开拓考核新店数量,现在会重点关注餐饮外卖“健康度”,对于营业额忽高忽低、在线排名变化的餐饮店,都需要BD专家根据大数据帮助餐饮店经营者找出原因并给出解决建议,避免新店外卖刚开始就淹没在区域竞争中,销量平平的新店会离开平台,通过机器学习把餐饮运营专家的经验、以及人看不到的隐含规律固化下来,以数据决策来发现餐饮店经营问题、产品差异定位,让餐饮商户尝到甜头,才愿意继续经营。举个例子,饿了么员工都喜欢楼下一家鸡排店的午餐,但大数据发现这家店的外卖营收并不如实体店那么火爆,9元“鸡排+酸梅汁”是所有人都喜欢的爆款产品,可为什么同样菜品遭遇“线下火、线上冷”呢?数据预警后,BD顾问指出线上外卖鸡排产品没有写明“含免费酸梅汁一杯”的关键促销内容,导致大多数外卖消费者订一份鸡排一杯酸梅汁,却收到一份鸡排两杯酸梅汁,体验自然不好。 饿了么是数据驱动、智能算法调度的自动化生活服务平台,通过O2O数据的在线实时分析,与阿里云人工智能团队不断改进算法,以“全局最优”取代“局部最优”,保证平台上所有餐饮商户都能享受到数据智能的科技红利。 “上云用数”的外部价值诸多,从饿了么内部反馈来看,上云不仅没有让运维团队失去价值,反而带来了“云原生应用”(Cloud Native Application)、“云上多活”、“CDN云端压测”、“安全风控一体化”等创新路径与方案,通过敏捷基础设施(IaaS)、微服务架构(PaaS和SaaS)、持续交付管理、DevOps等云最佳实践,摆脱“人肉”支撑的种种困境,进而实现更快的上线速度、细致的故障探测和发现、故障时能自动隔离、故障时能够自动恢复、方便的水平扩容。饿了么CTO张雪峰先生说:“互联网平台型组织,业务量涨数倍,企业人数稳定降低,才是技术驱动的正确商业模式。” 在不久的将来,你每天订餐、出行、娱乐、工作留下的大数据,会“驯养”出无处不在、无所不能的智能机器人管家,家庭助理帮你点菜,无人机为你送餐,聊天机器人接受你的投诉……当然这个无比美妙的“未来世界”背后,皆有阿里云的数据智能母体“ET”。 本篇文章为转载内容。原文链接:https://blog.csdn.net/weixin_34126557/article/details/90592502。 该文由互联网用户投稿提供,文中观点代表作者本人意见,并不代表本站的立场。 作为信息平台,本站仅提供文章转载服务,并不拥有其所有权,也不对文章内容的真实性、准确性和合法性承担责任。 如发现本文存在侵权、违法、违规或事实不符的情况,请及时联系我们,我们将第一时间进行核实并删除相应内容。
2023-01-31 14:48:26
343
转载
转载文章
...Java中间件一面 技术一面考察范围 重点问了Java线程锁:synchronized 和ReentrantLock相关的底层实现 线程池的底层实现以及常见的参数 数据结构基本都问了一遍:链表、队列等 Java内存模型:常问的JVM分代模型,以及JDK1.8后的区别,最后还问了JVM相关的调优参数 分布式锁的实现比较技术 一面题目 自我介绍 擅长哪方面的技术? java有哪些锁中类?(乐观锁&悲观锁、可重入锁&Synchronize等)。 比较重要的数据结构,如链表,队列,栈的基本原理及大致实现 J.U.C下的常见类的使用。Threadpool的深入考察;blockingQueue的使用 Java内存分代模型,GC算法,JVM常见的启动参数;CMS算法的过程。 Volatile关键字有什么用(包括底层原理) 线程池的调优策略 Spring cloud的服务注册与发现是怎么设计的? 分布式系统的全局id如何实现 分布式锁的方案,redis和zookeeper那个好,如果是集群部署,高并发情况下那个性能更好。 1.2 Java中间件二面 技术二面考察范围: 问了项目相关的技术实现细节 数据库相关:索引、索引底层实现、mysql相关的行锁、表锁等 redis相关:架构设计、数据一致性问题 容器:容器的设计原理等技术 二面题目: 参与的项目,选一个,技术难度在哪里? Collections.sort底层排序方式 负载均衡的原理设计模式与重构,谈谈你对重构的理解 谈谈redis相关的集群有哪些成熟方案? 再谈谈一致hash算法(redis)? 数据库索引,B+树的特性和建树过程 Mysql相关的行锁,表锁;乐观锁,悲观锁 谈谈多线程和并发工具的使用 谈谈redis的架构和组件 Redis的数据一致性问题(分布式多节点环境&单机环境) Docker容器 1.3 Java中间件三面 技术三面考察范围: 主要谈到了高并发的实现方案 以及中间件:redis、rocketmq、kafka等的架构设计思路 最后问了平时怎么提升技术的技术 三面题目 高并发情况下,系统是如何支撑大量的请求的? 接着上面的问题,延伸到了中间件,kafka、redis、rocketmq、mycat等设计思路和适用场景等 最近上过哪些技术网站;最近再看那些书。 工作和生活中遇见最大的挑战,怎么去克服? 未来有怎样的打算 1.4 Java中间件四面 最后,你懂的,主要就是HR走流程了,主要问了未来的职业规划。 02 头条Java后台3面 2.1 头条一面 讲讲jvm运行时数据库区 讲讲你知道的垃圾回收算法 jvm内存模型jmm 内存泄漏与内存溢出的区别 select、epool 的区别?底层的数据结构是什么? mysql数据库默认存储引擎,有什么优点 优化数据库的方法,从sql到缓存到cpu到操作系统,知道多少说多少 什么情景下做分表,什么情景下做分库 linkedList与arrayList区别 适用场景 array list是如何扩容的 volatile 关键字的作用?Java 内存模型? java lock的实现,公平锁、非公平锁 悲观锁和乐观锁,应用中的案例,mysql当中怎么实现,java中的实现 2.2 头条二面 Java 内存分配策略? 多个线程同时请求内存,如何分配? Redis 底层用到了哪些数据结构? 使用 Redis 的 set 来做过什么? Redis 使用过程中遇到什么问题? 搭建过 Redis 集群吗? 如何分析“慢查询”日志进行 SQL/索引 优化? MySQL 索引结构解释一下?(B+ 树) MySQL Hash 索引适用情况?举下例子? 2.3 头条三面 如何保证数据库与redis缓存一致的Redis 的并发竞争问题是什么? 如何解决这个问题? 了解 Redis 事务的 CAS 方案吗? 如何保证 Redis 高并发、高可用? Redis 的主从复制原理,以及Redis 的哨兵原理? 如果让你写一个消息队列,该如何进行架构设计啊?说一下你的思路。 MySQL数据库主从同步怎么实现? 秒杀模块怎么设计的,如何压测,抗压手段 03 今日头条Java后台研发三面 3.1 一面 concurrent包下面用过哪些? countdownlatch功能实现 synchronized和lock区别,重入锁thread和runnable的区别 AtomicInteger实现原理(CAS自旋) java并发sleep与wait、notify与notifyAll的区别 如何实现高效的同步链表 java都有哪些加锁方式(synchronized、ReentrantLock、共享锁、读写锁等) 设计模式(工厂模式、单例模式(几种情况)、适配器模式、装饰者模式) maven依赖树,maven的依赖传递,循环依赖 3.2 二面 synchronized和reentrantLock的区别,synchronized用在代码快、方法、静态方法时锁的都是什么? 介绍spring的IOC和AOP,分别如何实现(classloader、动态代理)JVM的内存布局以及垃圾回收原理及过程 讲一下,讲一下CMS垃圾收集器垃圾回收的流程,以及CMS的缺点 redis如何处理分布式服务器并发造成的不一致OSGi的机制spring中bean加载机制,bean生成的具体步骤,ioc注入的方式spring何时创建- applicationContextlistener是监听哪个事件? 介绍ConcurrentHashMap原理,用的是哪种锁,segment有没可能增大? 解释mysql索引、b树,为啥不用平衡二叉树、红黑树 Zookeeper如何同步配置 3.3 三面 Java线程池ThreadPoolEcecutor参数,基本参数,使用场景 MySQL的ACID讲一下,延伸到隔离级别 dubbo的实现原理,说说RPC的要点 GC停顿原因,如何降低停顿? JVM如何调优、参数怎么调? 如何用工具分析jvm状态(visualVM看堆中对象的分配,对象间的引用、是否有内存泄漏,jstack看线程状态、是否死锁等等) 描述一致性hash算法 分布式雪崩场景如何避免? 再谈谈消息队列 04 抖音Java 三面 4.1 一面: hashmap,怎么扩容,怎么处理数据冲突? 怎么高效率的实现数据迁移? Linux的共享内存如何实现,大概说了一下。 socket网络编程,说一下TCP的三次握手和四次挥手同步IO和异步IO的区别? Java GC机制?GC Roots有哪些? 红黑树讲一下,五个特性,插入删除操作,时间复杂度? 快排的时间复杂度,最坏情况呢,最好情况呢,堆排序的时间复杂度呢,建堆的复杂度是多少 4.2 二面: 自我介绍,主要讲讲做了什么和擅长什么 设计模式了解哪些? AtomicInteger怎么实现原子修改的? ConcurrentHashMap 在Java7和Java8中的区别? 为什么Java8并发效率更好?什么情况下用HashMap,什么情况用ConcurrentHashMap? redis数据结构? redis数据淘汰机制? 4.3 三面(约五十分钟): mysql实现事务的原理(MVCC) MySQL数据主从同步是如何实现的? MySQL索引的实现,innodb的索引,b+树索引是怎么实现的,为什么用b+树做索引节点,一个节点存了多少数据,怎么规定大小,与磁盘页对应。 如果Redis有1亿个key,使用keys命令是否会影响线上服务? Redis的持久化方式,aod和rdb,具体怎么实现,追加日志和备份文件,底层实现原理的话知道么? 遇到最大困难是什么?怎么克服? 未来的规划是什么? 你想问我什么? 05 百度三面 5.1 百度一面 自我介绍 Java中的多态 为什么要同时重写hashcode和equals Hashmap的原理 Hashmap如何变线程安全,每种方式的优缺点 垃圾回收机制 Jvm的参数你知道的说一下 设计模式了解的说一下啊 手撕一个单例模式 手撕算法:反转单链表 手撕算法:实现类似微博子结构的数据结构,输入一系列父子关系,输出一个类似微博评论的父子结构图 手写java多线程 手写java的soeket编程,服务端和客户端 手撕算法: 爬楼梯,写出状态转移方程 智力题:时针分针什么时候重合 5.2 百度二面(现场) 自我介绍 项目介绍 服务器如何负载均衡,有哪些算法,哪个比较好,一致性哈希原理,怎么避免DDOS攻击请求打到少数机器。 TCP连接中的三次握手和四次挥手,四次挥手的最后一个ack的作用是什么,为什么要time wait,为什么是2msl。 数据库的备份和恢复怎么实现的,主从复制怎么做的,什么时候会出现数据不一致,如何解决。 Linux查看cpu占用率高的进程 手撕算法:给定一个数字三角形,找到从顶部到底部的最小路径和。每一步可以移动到下面一行的相邻数字上。 然后继续在这个问题上扩展 求出最短那条的路径 递归求出所有的路径 设计模式讲一下熟悉的 会不会滥用设计模式 多线程条件变量为什么要在while体里 你遇到什么挫折,怎么应对和处理 5.3 百度三面(现场) 自我介绍 项目介绍 Redis的特点 Redis的持久化怎么做,aof和rdb,有什么区别,有什么优缺点。 Redis使用哨兵部署会有什么问题,我说需要扩容的话还是得集群部署。 说一下JVM内存模型把,有哪些区,分别干什么的 说一下gc算法,分代回收说下 MySQL的引擎讲一下,有什么区别,使用场景呢 分布式事务了解么 反爬虫的机制,有哪些方式 06 蚂蚁中间件团队面试题 6.1 蚂蚁中间件一面: 自我介绍 JVM垃圾回收算法和垃圾回收器有哪些,最新的JDK采用什么算法。 新生代和老年代的回收机制。 讲一下ArrayList和linkedlist的区别,ArrayList与HashMap的扩容方式。 Concurrenthashmap1.8后的改动。 Java中的多线程,以及线程池的增长策略和拒绝策略了解么。 Tomcat的类加载器了解么 Spring的ioc和aop,Springmvc的基本架构,请求流程。 HTTP协议与Tcp有什么区别,http1.0和2.0的区别。 Java的网络编程,讲讲NIO的实现方式,与BIO的区别,以及介绍常用的NIO框架。 索引什么时候会失效变成全表扫描 介绍下分布式的paxos和raft算法 6.2 蚂蚁中间件二面 你在项目中怎么用到并发的。 消息队列的使用场景,谈谈Kafka。 你说了解分布式服务,那么你怎么理解分布式服务。 Dubbo和Spring Clound的区别,以及使用场景。 讲一下docker的实现原理,以及与JVM的区别。 MongoDB、Redis和Memcached的应用场景,各自优势 MongoDB有事务吗 Redis说一下sorted set底层原理 讲讲Netty为什么并发高,相关的核心组件有哪些 6.3 蚂蚁中间件三面 完整的画一个分布式集群部署图,从负载均衡到后端数据库集群。 分布式锁的方案,Redis和Zookeeper哪个好,如果是集群部署,高并发情况下哪个性能更好。 分布式系统的全局id如何实现。 数据库万级变成亿级,你如何来解决。 常见的服务器雪崩是由什么引起的,如何来防范。 异地容灾怎么实现 常用的高并发技术解决方案有哪些,以及对应的解决步骤。 07 京东4面(Java研发) 7.1 一面(基础面:约1小时) 自我介绍,主要讲讲做了什么和擅长什么 springmvc和spring-boot区别 @Autowired的实现原理 Bean的默认作用范围是什么?其他的作用范围? 索引是什么概念有什么作用?MySQL里主要有哪些索引结构?哈希索引和B+树索引比较? Java线程池的原理?线程池有哪些?线程池工厂有哪些线程池类型,及其线程池参数是什么? hashmap原理,处理哈希冲突用的哪种方法? 还知道什么处理哈希冲突的方法? Java GC机制?GC Roots有哪些? Java怎么进行垃圾回收的?什么对象会进老年代?垃圾回收算法有哪些?为什么新生代使用复制算法? HashMap的时间复杂度?HashMap中Hash冲突是怎么解决的?链表的上一级结构是什么?Java8中的HashMap有什么变化?红黑树需要比较大小才能进行插入,是依据什么进行比较的?其他Hash冲突解决方式? hash和B+树的区别?分别应用于什么场景?哪个比较好? 项目里有个数据安全的,aes和md5的区别?详细点 7.2 二面(问数据库较多) 自我介绍 为什么MyISAM查询性能好? 事务特性(acid) 隔离级别 SQL慢查询的常见优化步骤? 说下乐观锁,悲观锁(select for update),并写出sql实现 TCP协议的三次握手和四次挥手过程? 用到过哪些rpc框架 数据库连接池怎么实现 Java web过滤器的生命周期 7.3 三面(综合面;约一个小时) 自我介绍。 ConcurrentHashMap 在Java7和Java8中的区别?为什么Java8并发效率更好?什么情况下用HashMap,什么情况用ConcurrentHashMap? 加锁有什么机制? ThreadLocal?应用场景? 数据库水平切分,垂直切分的设计思路和切分顺序 Redis如何解决key冲突 soa和微服务的区别? 单机系统演变为分布式系统,会涉及到哪些技术的调整?请从前面负载到后端详细描述。 设计一个秒杀系统? 7.4 四面(HR面) 你自己最大优势和劣势是什么 平时遇见过什么样的挑战,怎么去克服的 工作中遇见了技术解决不了的问题,你的应对思路? 你的兴趣爱好? 未来的职业规划是什么? 08 美团java高级开发3面 8.1 美团一面 自我介绍 项目介绍 Redis介绍 了解redis源码么 了解redis集群么 Hashmap的原理,增删的情况后端数据结构如何位移 hashmap容量为什么是2的幂次 hashset的源码 object类你知道的方法 hashcode和equals 你重写过hashcode和equals么,要注意什么 假设现在一个学生类,有学号和姓名,我现在hashcode方法重写的时候,只将学号参与计算,会出现什么情况? 往set里面put一个学生对象,然后将这个学生对象的学号改了,再put进去,可以放进set么?并讲出为什么 Redis的持久化?有哪些方式,原理是什么? 讲一下稳定的排序算法和不稳定的排序算法 讲一下快速排序的思想 8.2 美团二面 自我介绍 讲一下数据的acid 什么是一致性 什么是隔离性 Mysql的隔离级别 每个隔离级别是如何解决 Mysql要加上nextkey锁,语句该怎么写 Java的内存模型,垃圾回收 线程池的参数 每个参数解释一遍 然后面试官设置了每个参数,给了是个线程,让描述出完整的线程池执行的流程 Nio和IO有什么区别 Nio和aio的区别 Spring的aop怎么实现 Spring的aop有哪些实现方式 动态代理的实现方式和区别 Linux了解么 怎么查看系统负载 Cpu load的参数如果为4,描述一下现在系统处于什么情况 Linux,查找磁盘上最大的文件的命令 Linux,如何查看系统日志文件 手撕算法:leeetcode原题 22,Generate Parentheses,给定 n 对括号,请- 写一个函数以将其生成新的括号组合,并返回所有组合结果。 8.3 美团三面(现场) 三面没怎么问技术,问了很多技术管理方面的问题 自我介绍 项目介绍 怎么管理项目成员 当意见不一致时,如何沟通并说服开发成员,并举个例子 怎么保证项目的进度 数据库的索引原理 非聚簇索引和聚簇索引 索引的使用注意事项 联合索引 从底层解释最左匹配原则 Mysql对联合索引有优化么?会自动调整顺序么?哪个版本开始优化? Redis的应用 Redis的持久化的方式和原理 技术选型,一个新技术和一个稳定的旧技术,你会怎么选择,选择的考虑有哪些 说你印象最深的美团点评技术团队的三篇博客 最近在学什么新技术 你是怎么去接触一门新技术的 会看哪些书 怎么选择要看的书 最后 由于篇幅限制,小编在此截出几张知识讲解的图解,有需要的程序猿(媛)可以点赞后戳这里免费领取全部资料获取哦 子 怎么保证项目的进度 数据库的索引原理 非聚簇索引和聚簇索引 索引的使用注意事项 联合索引 从底层解释最左匹配原则 Mysql对联合索引有优化么?会自动调整顺序么?哪个版本开始优化? Redis的应用 Redis的持久化的方式和原理 技术选型,一个新技术和一个稳定的旧技术,你会怎么选择,选择的考虑有哪些 说你印象最深的美团点评技术团队的三篇博客 最近在学什么新技术 你是怎么去接触一门新技术的 会看哪些书 怎么选择要看的书 最后 由于篇幅限制,小编在此截出几张知识讲解的图解,有需要的程序猿(媛)可以点赞后戳这里免费领取全部资料获取哦 [外链图片转存中…(img-SFREePIJ-1624074891834)] [外链图片转存中…(img-5kF3pkiC-1624074891834)] [外链图片转存中…(img-HDVXfOMR-1624074891835)] [外链图片转存中…(img-RyaAC5jy-1624074891836)] [外链图片转存中…(img-iV32C5Ok-1624074891837)] 本篇文章为转载内容。原文链接:https://blog.csdn.net/m0_57285325/article/details/118051767。 该文由互联网用户投稿提供,文中观点代表作者本人意见,并不代表本站的立场。 作为信息平台,本站仅提供文章转载服务,并不拥有其所有权,也不对文章内容的真实性、准确性和合法性承担责任。 如发现本文存在侵权、违法、违规或事实不符的情况,请及时联系我们,我们将第一时间进行核实并删除相应内容。
2023-11-13 23:43:59
85
转载
转载文章
...宝和余额宝使用不同的数据库 如图: 2、分布式事务解决方案 1、基于数据库XA协议的两段提交 XA协议是数据库支持的一种协议,其核心是一个事务管理器用来统一管理两个分布式数据库,如图 事务管理器负责跟支付宝数据库和余额宝数据库打交道,一旦有一个数据库连接失败,另一个数据库的操作就不会进行,一个数据库操作失败就会导致另一个数据库回滚,只有他们全部成功两个数据库的事务才会提交。 基于XA协议的两段和三段提交是一种严格的安全确认机制,其安全性是非常高的,但是保证安全性的前提是牺牲了性能,这个就是分布式系统里面的CAP理论,做任何架构的前提需要有取舍。所以基于XA协议的分布式事务并发性不高,不适合高并发场景。 2、基于activemq的解决方案 如图: 1、支付宝扣款成功时往message表插入消息 2、message表有message_id(流水id,标识夸系统的一次转账操作),status(confirm,unconfirm) 3、timer扫描message表的unconfirm状态记录往activemq插入消息 4、余额宝收到消息消费消息时先查询message表如果有记录就不处理如果没记录就进行数据库增款操作 5、如果余额宝数据库操作成功往余额宝message表插入消息,表字段跟支付宝message一致 6、如果5操作成功,回调支付宝接口修改message表状态,把unconfirm状态转换成confirm状态 问题描述: 1、支付宝设计message表的目的 如果支付宝往activemq插入消息而余额宝消费消息异常,有可能是消费消息成功而事务操作异常,有可能是网络异常等等不确定因素。如果出现异常而activemq收到了确认消息的信号,这时候activemq中的消息是删除了的,消息丢失了。设置message表就是有一个消息存根,activemq中消息丢失了message表中的消息还在。解决了activemq消息丢失问题 2、余额宝设计message表的目的 当余额宝消费成功并且数据库操作成功时,回调支付宝的消息确认接口,如果回调接口时出现异常导致支付宝状态修改失败还是unconfirm状态,这时候还会被timer扫描到,又会往activemq插入消息,又会被余额宝消费一边,但是这条消息已经消费成功了的只是回调失败而已,所以就需要有一个这样的message表,当余额宝消费时先插入message表,如果message根据message_id能查询到记录就说明之前这条消息被消费过就不再消费只需要回调成功即可,如果查询不到消息就消费这条消息继续数据库操作,数据库操作成功就往message表插入消息。 这样就解决了消息重复消费问题,这也是消费端的幂等操作。 基于消息中间件的分布式事务是最理想的分布式事务解决方案,兼顾了安全性和并发性! 接下来贴代码: 支付宝代码: @Controller@RequestMapping("/order")public class OrderController {/ @Description TODO @param @return 参数 @return String 返回类型 @throws userID:转账的用户ID amount:转多少钱/@Autowired@Qualifier("activemq")OrderService orderService;@RequestMapping("/transfer")public @ResponseBody String transferAmount(String userId,String messageId, int amount) {try {orderService.updateAmount(amount,messageId, userId);}catch (Exception e) {e.printStackTrace();return "===============================transferAmount failed===================";}return "===============================transferAmount successfull===================";}@RequestMapping("/callback")public String callback(String param) {JSONObject parse = JSONObject.parseObject(param);String respCode = parse.getString("respCode");if(!"OK".equalsIgnoreCase(respCode)) {return null;}try {orderService.updateMessage(param);}catch (Exception e) {e.printStackTrace();return "fail";}return "ok";} } public interface OrderService {public void updateAmount(int amount, String userId,String messageId);public void updateMessage(String param);} @Service("activemq")@Transactional(rollbackFor = Exception.class)public class OrderServiceActivemqImpl implements OrderService {Logger logger = LoggerFactory.getLogger(getClass());@AutowiredJdbcTemplate jdbcTemplate;@AutowiredJmsTemplate jmsTemplate;@Overridepublic void updateAmount(final int amount, final String messageId, final String userId) {String sql = "update account set amount = amount - ?,update_time=now() where user_id = ?";int count = jdbcTemplate.update(sql, new Object[]{amount, userId});if (count == 1) {//插入到消息记录表sql = "insert into message(user_id,message_id,amount,status) values (?,?,?,?)";int row = jdbcTemplate.update(sql,new Object[]{userId,messageId,amount,"unconfirm"});if(row == 1) {//往activemq中插入消息jmsTemplate.send("zg.jack.queue", new MessageCreator() {@Overridepublic Message createMessage(Session session) throws JMSException {com.zhuguang.jack.bean.Message message = new com.zhuguang.jack.bean.Message();message.setAmount(Integer.valueOf(amount));message.setStatus("unconfirm");message.setUserId(userId);message.setMessageId(messageId);return session.createObjectMessage(message);} });} }}@Overridepublic void updateMessage(String param) {JSONObject parse = JSONObject.parseObject(param);String messageId = parse.getString("messageId");String sql = "update message set status = ? where message_id = ?";int count = jdbcTemplate.update(sql,new Object[]{"confirm",messageId});if(count == 1) {logger.info(messageId + " callback successfull");} }} activemq.xml <?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:amq="http://activemq.apache.org/schema/core"xmlns:jms="http://www.springframework.org/schema/jms"xmlns:context="http://www.springframework.org/schema/context"xmlns:mvc="http://www.springframework.org/schema/mvc"xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans-4.1.xsdhttp://www.springframework.org/schema/contexthttp://www.springframework.org/schema/context/spring-context-4.1.xsdhttp://www.springframework.org/schema/mvchttp://www.springframework.org/schema/mvc/spring-mvc-4.1.xsdhttp://www.springframework.org/schema/jmshttp://www.springframework.org/schema/jms/spring-jms-4.1.xsdhttp://activemq.apache.org/schema/corehttp://activemq.apache.org/schema/core/activemq-core-5.12.1.xsd"><context:component-scan base-package="com.zhuguang.jack" /><mvc:annotation-driven /><amq:connectionFactory id="amqConnectionFactory"brokerURL="tcp://192.168.88.131:61616"userName="system"password="manager" /><!-- 配置JMS连接工长 --><bean id="connectionFactory"class="org.springframework.jms.connection.CachingConnectionFactory"><constructor-arg ref="amqConnectionFactory" /><property name="sessionCacheSize" value="100" /></bean><!-- 定义消息队列(Queue) --><bean id="demoQueueDestination" class="org.apache.activemq.command.ActiveMQQueue"><!-- 设置消息队列的名字 --><constructor-arg><value>zg.jack.queue</value></constructor-arg></bean><!-- 配置JMS模板(Queue),Spring提供的JMS工具类,它发送、接收消息。 --><bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate"><property name="connectionFactory" ref="connectionFactory" /><property name="defaultDestination" ref="demoQueueDestination" /><property name="receiveTimeout" value="10000" /><!-- true是topic,false是queue,默认是false,此处显示写出false --><property name="pubSubDomain" value="false" /></bean></beans> spring-dispatcher.xml <beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"xmlns:context="http://www.springframework.org/schema/context"xmlns:task="http://www.springframework.org/schema/task" xmlns:aop="http://www.springframework.org/schema/aop"xmlns:tx="http://www.springframework.org/schema/tx"xmlns:util="http://www.springframework.org/schema/util" xmlns:mvc="http://www.springframework.org/schema/mvc"xsi:schemaLocation="http://www.springframework.org/schema/utilhttp://www.springframework.org/schema/util/spring-util-3.2.xsdhttp://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsdhttp://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.2.xsdhttp://www.springframework.org/schema/mvchttp://www.springframework.org/schema/mvc/spring-mvc-3.2.xsdhttp://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-3.0.xsdhttp://www.springframework.org/schema/txhttp://www.springframework.org/schema/tx/spring-tx-3.0.xsdhttp://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd"><!-- 引入同文件夹下的redis属性配置文件 --><!-- 解决springMVC响应数据乱码 text/plain就是响应的时候原样返回数据--><import resource="../activemq/activemq.xml"/><!--<context:property-placeholder ignore-unresolvable="true" location="classpath:config/core/core.properties,classpath:config/redis/redis-config.properties" />--><bean id="propertyConfigurerForProject1" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"><property name="order" value="1" /><property name="ignoreUnresolvablePlaceholders" value="true" /><property name="location"><value>classpath:config/core/core.properties</value></property></bean><mvc:annotation-driven><mvc:message-converters register-defaults="true"><bean class="org.springframework.http.converter.StringHttpMessageConverter"><property name="supportedMediaTypes" value = "text/plain;charset=UTF-8" /></bean></mvc:message-converters></mvc:annotation-driven><!-- 避免IE执行AJAX时,返回JSON出现下载文件 --><bean id="mappingJacksonHttpMessageConverter" class="org.springframework.http.converter.json.MappingJacksonHttpMessageConverter"><property name="supportedMediaTypes"><list><value>text/html;charset=UTF-8</value></list></property></bean><!-- 开启controller注解支持 --><!-- 注:如果base-package=com.avicit 则注解事务不起作用 TODO 读源码 --><context:component-scan base-package="com.zhuguang"></context:component-scan><mvc:view-controller path="/" view-name="redirect:/index" /><beanclass="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping" /><bean id="handlerAdapter"class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter"></bean><beanclass="org.springframework.web.servlet.view.ContentNegotiatingViewResolver"><property name="mediaTypes"><map><entry key="json" value="application/json" /><entry key="xml" value="application/xml" /><entry key="html" value="text/html" /></map></property><property name="viewResolvers"><list><bean class="org.springframework.web.servlet.view.BeanNameViewResolver" /><bean class="org.springframework.web.servlet.view.UrlBasedViewResolver"><property name="viewClass" value="org.springframework.web.servlet.view.JstlView" /><property name="prefix" value="/" /><property name="suffix" value=".jsp" /></bean></list></property></bean><!-- 支持上传文件 --> <!-- 控制器异常处理 --><bean id="exceptionResolver"class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver"><property name="exceptionMappings"><props><prop key="java.lang.Exception">error</prop></props></property></bean><bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close"><property name="driverClass"><value>${jdbc.driverClassName}</value></property><property name="jdbcUrl"><value>${jdbc.url}</value></property><property name="user"><value>${jdbc.username}</value></property><property name="password"><value>${jdbc.password}</value></property><property name="minPoolSize" value="10" /><property name="maxPoolSize" value="100" /><property name="maxIdleTime" value="1800" /><property name="acquireIncrement" value="3" /><property name="maxStatements" value="1000" /><property name="initialPoolSize" value="10" /><property name="idleConnectionTestPeriod" value="60" /><property name="acquireRetryAttempts" value="30" /><property name="breakAfterAcquireFailure" value="false" /><property name="testConnectionOnCheckout" value="false" /><property name="acquireRetryDelay"><value>100</value></property></bean><bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"><property name="dataSource" ref="dataSource"></property></bean><bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"><property name="dataSource" ref="dataSource"/></bean><tx:annotation-driven transaction-manager="transactionManager" proxy-target-class="true" /><aop:aspectj-autoproxy expose-proxy="true"/></beans> logback.xml <?xml version="1.0" encoding="UTF-8"?><!--scan:当此属性设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true。scanPeriod:设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒当scan为true时,此属性生效。默认的时间间隔为1分钟。debug:当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。--><configuration scan="false" scanPeriod="60 seconds" debug="false"><!-- 定义日志的根目录 --><!-- <property name="LOG_HOME" value="/app/log" /> --><!-- 定义日志文件名称 --><property name="appName" value="netty"></property><!-- ch.qos.logback.core.ConsoleAppender 表示控制台输出 --><appender name="stdout" class="ch.qos.logback.core.ConsoleAppender"><Encoding>UTF-8</Encoding><!--日志输出格式:%d表示日期时间,%thread表示线程名,%-5level:级别从左显示5个字符宽度%logger{50} 表示logger名字最长50个字符,否则按照句点分割。 %msg:日志消息,%n是换行符--><encoder><pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern></encoder></appender><!-- 滚动记录文件,先将日志记录到指定文件,当符合某个条件时,将日志记录到其他文件 --> <appender name="appLogAppender" class="ch.qos.logback.core.rolling.RollingFileAppender"><Encoding>UTF-8</Encoding><!-- 指定日志文件的名称 --> <file>${appName}.log</file><!--当发生滚动时,决定 RollingFileAppender 的行为,涉及文件移动和重命名TimeBasedRollingPolicy: 最常用的滚动策略,它根据时间来制定滚动策略,既负责滚动也负责出发滚动。--><rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"><!--滚动时产生的文件的存放位置及文件名称 %d{yyyy-MM-dd}:按天进行日志滚动 %i:当文件大小超过maxFileSize时,按照i进行文件滚动--><fileNamePattern>${appName}-%d{yyyy-MM-dd}-%i.log</fileNamePattern><!-- 可选节点,控制保留的归档文件的最大数量,超出数量就删除旧文件。假设设置每天滚动,且maxHistory是365,则只保存最近365天的文件,删除之前的旧文件。注意,删除旧文件是,那些为了归档而创建的目录也会被删除。--><MaxHistory>365</MaxHistory><!-- 当日志文件超过maxFileSize指定的大小是,根据上面提到的%i进行日志文件滚动 注意此处配置SizeBasedTriggeringPolicy是无法实现按文件大小进行滚动的,必须配置timeBasedFileNamingAndTriggeringPolicy--><timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"><maxFileSize>100MB</maxFileSize></timeBasedFileNamingAndTriggeringPolicy></rollingPolicy><!--日志输出格式:%d表示日期时间,%thread表示线程名,%-5level:级别从左显示5个字符宽度 %logger{50} 表示logger名字最长50个字符,否则按照句点分割。 %msg:日志消息,%n是换行符--> <encoder><pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [ %thread ] - [ %-5level ] [ %logger{50} : %line ] - %msg%n</pattern></encoder></appender><!-- logger主要用于存放日志对象,也可以定义日志类型、级别name:表示匹配的logger类型前缀,也就是包的前半部分level:要记录的日志级别,包括 TRACE < DEBUG < INFO < WARN < ERRORadditivity:作用在于children-logger是否使用 rootLogger配置的appender进行输出,false:表示只用当前logger的appender-ref,true:表示当前logger的appender-ref和rootLogger的appender-ref都有效--><!-- <logger name="edu.hyh" level="info" additivity="true"><appender-ref ref="appLogAppender" /></logger> --><!-- root与logger是父子关系,没有特别定义则默认为root,任何一个类只会和一个logger对应,要么是定义的logger,要么是root,判断的关键在于找到这个logger,然后判断这个logger的appender和level。 --><root level="debug"><appender-ref ref="stdout" /><appender-ref ref="appLogAppender" /></root></configuration> 2、余额宝代码 package com.zhuguang.jack.controller;import com.alibaba.fastjson.JSONObject;import com.zhuguang.jack.service.OrderService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.ResponseBody;@Controller@RequestMapping("/order")public class OrderController {/ @Description TODO @param @return 参数 @return String 返回类型 @throws 模拟银行转账 userID:转账的用户ID amount:转多少钱/@AutowiredOrderService orderService;@RequestMapping("/transfer")public @ResponseBody String transferAmount(String userId, String amount) {try {orderService.updateAmount(Integer.valueOf(amount), userId);}catch (Exception e) {e.printStackTrace();return "===============================transferAmount failed===================";}return "===============================transferAmount successfull===================";} } 消息监听器 package com.zhuguang.jack.listener;import com.alibaba.fastjson.JSONObject;import com.zhuguang.jack.service.OrderService;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.http.client.SimpleClientHttpRequestFactory;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Transactional;import org.springframework.web.client.RestTemplate;import javax.jms.JMSException;import javax.jms.Message;import javax.jms.MessageListener;import javax.jms.ObjectMessage;@Service("queueMessageListener")public class QueueMessageListener implements MessageListener {private Logger logger = LoggerFactory.getLogger(getClass());@AutowiredOrderService orderService;@Transactional(rollbackFor = Exception.class)@Overridepublic void onMessage(Message message) {if (message instanceof ObjectMessage) {ObjectMessage objectMessage = (ObjectMessage) message;try {com.zhuguang.jack.bean.Message message1 = (com.zhuguang.jack.bean.Message) objectMessage.getObject();String userId = message1.getUserId();int count = orderService.queryMessageCountByUserId(userId);if (count == 0) {orderService.updateAmount(message1.getAmount(), message1.getUserId());orderService.insertMessage(message1.getUserId(), message1.getMessageId(), message1.getAmount(), "ok");} else {logger.info("异常转账");}RestTemplate restTemplate = createRestTemplate();JSONObject jo = new JSONObject();jo.put("messageId", message1.getMessageId());jo.put("respCode", "OK");String url = "http://jack.bank_a.com:8080/alipay/order/callback?param="+ jo.toJSONString();restTemplate.getForObject(url,null);} catch (JMSException e) {e.printStackTrace();throw new RuntimeException("异常");} }}public RestTemplate createRestTemplate() {SimpleClientHttpRequestFactory simpleClientHttpRequestFactory = new SimpleClientHttpRequestFactory();simpleClientHttpRequestFactory.setConnectTimeout(3000);simpleClientHttpRequestFactory.setReadTimeout(2000);return new RestTemplate(simpleClientHttpRequestFactory);} } package com.zhuguang.jack.service;public interface OrderService {public void updateAmount(int amount, String userId);public int queryMessageCountByUserId(String userId);public int insertMessage(String userId,String messageId,int amount,String status);} package com.zhuguang.jack.service;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.http.client.SimpleClientHttpRequestFactory;import org.springframework.jdbc.core.JdbcTemplate;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Transactional;import org.springframework.web.client.RestTemplate;@Service@Transactional(rollbackFor = Exception.class)public class OrderServiceImpl implements OrderService {private Logger logger = LoggerFactory.getLogger(getClass());@AutowiredJdbcTemplate jdbcTemplate;/ 更新数据库表,把账户余额减去amountd/@Overridepublic void updateAmount(int amount, String userId) {//1、农业银行转账3000,也就说农业银行jack账户要减3000String sql = "update account set amount = amount + ?,update_time=now() where user_id = ?";int count = jdbcTemplate.update(sql, new Object[] {amount, userId});if (count != 1) {throw new RuntimeException("订单创建失败,农业银行转账失败!");} }public RestTemplate createRestTemplate() {SimpleClientHttpRequestFactory simpleClientHttpRequestFactory = new SimpleClientHttpRequestFactory();simpleClientHttpRequestFactory.setConnectTimeout(3000);simpleClientHttpRequestFactory.setReadTimeout(2000);return new RestTemplate(simpleClientHttpRequestFactory);}@Overridepublic int queryMessageCountByUserId(String messageId) {String sql = "select count() from message where message_id = ?";int count = jdbcTemplate.queryForInt(sql, new Object[]{messageId});return count;}@Overridepublic int insertMessage(String userId, String message_id,int amount, String status) {String sql = "insert into message(user_id,message_id,amount,status) values(?,?,?)";int count = jdbcTemplate.update(sql, new Object[]{userId, message_id,amount, status});if(count == 1) {logger.info("Ok");}return count;} } activemq.xml <?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:amq="http://activemq.apache.org/schema/core"xmlns:jms="http://www.springframework.org/schema/jms"xmlns:context="http://www.springframework.org/schema/context"xmlns:mvc="http://www.springframework.org/schema/mvc"xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans-4.1.xsdhttp://www.springframework.org/schema/contexthttp://www.springframework.org/schema/context/spring-context-4.1.xsdhttp://www.springframework.org/schema/mvchttp://www.springframework.org/schema/mvc/spring-mvc-4.1.xsdhttp://www.springframework.org/schema/jmshttp://www.springframework.org/schema/jms/spring-jms-4.1.xsdhttp://activemq.apache.org/schema/corehttp://activemq.apache.org/schema/core/activemq-core-5.12.1.xsd"><context:component-scan base-package="com.zhuguang.jack" /><mvc:annotation-driven /><amq:connectionFactory id="amqConnectionFactory"brokerURL="tcp://192.168.88.131:61616"userName="system"password="manager" /><!-- 配置JMS连接工长 --><bean id="connectionFactory"class="org.springframework.jms.connection.CachingConnectionFactory"><constructor-arg ref="amqConnectionFactory" /><property name="sessionCacheSize" value="100" /></bean><!-- 定义消息队列(Queue) --><bean id="demoQueueDestination" class="org.apache.activemq.command.ActiveMQQueue"><!-- 设置消息队列的名字 --><constructor-arg><value>zg.jack.queue</value></constructor-arg></bean><!-- 显示注入消息监听容器(Queue),配置连接工厂,监听的目标是demoQueueDestination,监听器是上面定义的监听器 --><bean id="queueListenerContainer"class="org.springframework.jms.listener.DefaultMessageListenerContainer"><property name="connectionFactory" ref="connectionFactory" /><property name="destination" ref="demoQueueDestination" /><property name="messageListener" ref="queueMessageListener" /></bean><!-- 配置JMS模板(Queue),Spring提供的JMS工具类,它发送、接收消息。 --><bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate"><property name="connectionFactory" ref="connectionFactory" /><property name="defaultDestination" ref="demoQueueDestination" /><property name="receiveTimeout" value="10000" /><!-- true是topic,false是queue,默认是false,此处显示写出false --><property name="pubSubDomain" value="false" /></bean></beans> OK~~~~~~~~~~~~大功告成!!!, 如果大家觉得满意并且对技术感兴趣请加群:171239762, 纯技术交流群,非诚勿扰。 本篇文章为转载内容。原文链接:https://blog.csdn.net/luoyang_java/article/details/84953241。 该文由互联网用户投稿提供,文中观点代表作者本人意见,并不代表本站的立场。 作为信息平台,本站仅提供文章转载服务,并不拥有其所有权,也不对文章内容的真实性、准确性和合法性承担责任。 如发现本文存在侵权、违法、违规或事实不符的情况,请及时联系我们,我们将第一时间进行核实并删除相应内容。
2023-04-16 22:34:52
499
转载
转载文章
...完全由指标评估,引入数据指标以外的要素也很重要。 比如广告和特型内容频控。像问答卡片就是比较特殊的内容形式,其推荐的目标不完全是让用户浏览,还要考虑吸引用户回答为社区贡献内容。这些内容和普通内容如何混排,怎样控制频控都需要考虑。 此外,平台出于内容生态和社会责任的考量,像低俗内容的打压,标题党、低质内容的打压,重要新闻的置顶、加权、强插,低级别账号内容降权都是算法本身无法完成,需要进一步对内容进行干预。 下面我将简单介绍在上述算法目标的基础上如何对其实现。 前面提到的公式y = F(Xi ,Xu ,Xc),是一个很经典的监督学习问题。可实现的方法有很多,比如传统的协同过滤模型,监督学习算法Logistic Regression模型,基于深度学习的模型,Factorization Machine和GBDT等。 一个优秀的工业级推荐系统需要非常灵活的算法实验平台,可以支持多种算法组合,包括模型结构调整。因为很难有一套通用的模型架构适用于所有的推荐场景。 现在很流行将LR和DNN结合,前几年Facebook也将LR和GBDT算法做结合。今日头条旗下几款产品都在沿用同一套强大的算法推荐系统,但根据业务场景不同,模型架构会有所调整。 模型之后再看一下典型的推荐特征,主要有四类特征会对推荐起到比较重要的作用。 第一类是相关性特征,就是评估内容的属性和与用户是否匹配。显性的匹配包括关键词匹配、分类匹配、来源匹配、主题匹配等。像FM模型中也有一些隐性匹配,从用户向量与内容向量的距离可以得出。 第二类是环境特征,包括地理位置、时间。这些既是bias特征,也能以此构建一些匹配特征。 第三类是热度特征。包括全局热度、分类热度,主题热度,以及关键词热度等。内容热度信息在大的推荐系统特别在用户冷启动的时候非常有效。 第四类是协同特征,它可以在部分程度上帮助解决所谓算法越推越窄的问题。 协同特征并非考虑用户已有历史。而是通过用户行为分析不同用户间相似性,比如点击相似、兴趣分类相似、主题相似、兴趣词相似,甚至向量相似,从而扩展模型的探索能力。 模型的训练上,头条系大部分推荐产品采用实时训练。实时训练省资源并且反馈快,这对信息流产品非常重要。用户需要行为信息可以被模型快速捕捉并反馈至下一刷的推荐效果。 我们线上目前基于storm集群实时处理样本数据,包括点击、展现、收藏、分享等动作类型。 模型参数服务器是内部开发的一套高性能的系统,因为头条数据规模增长太快,类似的开源系统稳定性和性能无法满足,而我们自研的系统底层做了很多针对性的优化,提供了完善运维工具,更适配现有的业务场景。 目前,头条的推荐算法模型在世界范围内也是比较大的,包含几百亿原始特征和数十亿向量特征。 整体的训练过程是线上服务器记录实时特征,导入到Kafka文件队列中,然后进一步导入Storm集群消费Kafka数据,客户端回传推荐的label构造训练样本,随后根据最新样本进行在线训练更新模型参数,最终线上模型得到更新。 这个过程中主要的延迟在用户的动作反馈延时,因为文章推荐后用户不一定马上看,不考虑这部分时间,整个系统是几乎实时的。 但因为头条目前的内容量非常大,加上小视频内容有千万级别,推荐系统不可能所有内容全部由模型预估。 所以需要设计一些召回策略,每次推荐时从海量内容中筛选出千级别的内容库。召回策略最重要的要求是性能要极致,一般超时不能超过50毫秒。 召回策略种类有很多,我们主要用的是倒排的思路。离线维护一个倒排,这个倒排的key可以是分类,topic,实体,来源等。 排序考虑热度、新鲜度、动作等。线上召回可以迅速从倒排中根据用户兴趣标签对内容做截断,高效的从很大的内容库中筛选比较靠谱的一小部分内容。 二、内容分析 内容分析包括文本分析,图片分析和视频分析。头条一开始主要做资讯,今天我们主要讲一下文本分析。文本分析在推荐系统中一个很重要的作用是用户兴趣建模。 没有内容及文本标签,无法得到用户兴趣标签。举个例子,只有知道文章标签是互联网,用户看了互联网标签的文章,才能知道用户有互联网标签,其他关键词也一样。 另一方面,文本内容的标签可以直接帮助推荐特征,比如魅族的内容可以推荐给关注魅族的用户,这是用户标签的匹配。 如果某段时间推荐主频道效果不理想,出现推荐窄化,用户会发现到具体的频道推荐(如科技、体育、娱乐、军事等)中阅读后,再回主feed,推荐效果会更好。 因为整个模型是打通的,子频道探索空间较小,更容易满足用户需求。只通过单一信道反馈提高推荐准确率难度会比较大,子频道做的好很重要。而这也需要好的内容分析。 上图是今日头条的一个实际文本case。可以看到,这篇文章有分类、关键词、topic、实体词等文本特征。 当然不是没有文本特征,推荐系统就不能工作,推荐系统最早期应用在Amazon,甚至沃尔玛时代就有,包括Netfilx做视频推荐也没有文本特征直接协同过滤推荐。 但对资讯类产品而言,大部分是消费当天内容,没有文本特征新内容冷启动非常困难,协同类特征无法解决文章冷启动问题。 今日头条推荐系统主要抽取的文本特征包括以下几类。首先是语义标签类特征,显式为文章打上语义标签。 这部分标签是由人定义的特征,每个标签有明确的意义,标签体系是预定义的。 此外还有隐式语义特征,主要是topic特征和关键词特征,其中topic特征是对于词概率分布的描述,无明确意义;而关键词特征会基于一些统一特征描述,无明确集合。 另外文本相似度特征也非常重要。在头条,曾经用户反馈最大的问题之一就是为什么总推荐重复的内容。这个问题的难点在于,每个人对重复的定义不一样。 举个例子,有人觉得这篇讲皇马和巴萨的文章,昨天已经看过类似内容,今天还说这两个队那就是重复。 但对于一个重度球迷而言,尤其是巴萨的球迷,恨不得所有报道都看一遍。解决这一问题需要根据判断相似文章的主题、行文、主体等内容,根据这些特征做线上策略。 同样,还有时空特征,分析内容的发生地点以及时效性。比如武汉限行的事情推给北京用户可能就没有意义。 最后还要考虑质量相关特征,判断内容是否低俗,色情,是否是软文,鸡汤? 上图是头条语义标签的特征和使用场景。他们之间层级不同,要求不同。 分类的目标是覆盖全面,希望每篇内容每段视频都有分类;而实体体系要求精准,相同名字或内容要能明确区分究竟指代哪一个人或物,但不用覆盖很全。 概念体系则负责解决比较精确又属于抽象概念的语义。这是我们最初的分类,实践中发现分类和概念在技术上能互用,后来统一用了一套技术架构。 目前,隐式语义特征已经可以很好的帮助推荐,而语义标签需要持续标注,新名词新概念不断出现,标注也要不断迭代。其做好的难度和资源投入要远大于隐式语义特征,那为什么还需要语义标签? 有一些产品上的需要,比如频道需要有明确定义的分类内容和容易理解的文本标签体系。语义标签的效果是检查一个公司NLP技术水平的试金石。 今日头条推荐系统的线上分类采用典型的层次化文本分类算法。 最上面Root,下面第一层的分类是像科技、体育、财经、娱乐,体育这样的大类,再下面细分足球、篮球、乒乓球、网球、田径、游泳…,足球再细分国际足球、中国足球,中国足球又细分中甲、中超、国家队…,相比单独的分类器,利用层次化文本分类算法能更好地解决数据倾斜的问题。 有一些例外是,如果要提高召回,可以看到我们连接了一些飞线。这套架构通用,但根据不同的问题难度,每个元分类器可以异构,像有些分类SVM效果很好,有些要结合CNN,有些要结合RNN再处理一下。 上图是一个实体词识别算法的case。基于分词结果和词性标注选取候选,期间可能需要根据知识库做一些拼接,有些实体是几个词的组合,要确定哪几个词结合在一起能映射实体的描述。 如果结果映射多个实体还要通过词向量、topic分布甚至词频本身等去歧,最后计算一个相关性模型。 三、用户标签 内容分析和用户标签是推荐系统的两大基石。内容分析涉及到机器学习的内容多一些,相比而言,用户标签工程挑战更大。 今日头条常用的用户标签包括用户感兴趣的类别和主题、关键词、来源、基于兴趣的用户聚类以及各种垂直兴趣特征(车型,体育球队,股票等)。还有性别、年龄、地点等信息。 性别信息通过用户第三方社交账号登录得到。年龄信息通常由模型预测,通过机型、阅读时间分布等预估。 常驻地点来自用户授权访问位置信息,在位置信息的基础上通过传统聚类的方法拿到常驻点。 常驻点结合其他信息,可以推测用户的工作地点、出差地点、旅游地点。这些用户标签非常有助于推荐。 当然最简单的用户标签是浏览过的内容标签。但这里涉及到一些数据处理策略。 主要包括: 一、过滤噪声。通过停留时间短的点击,过滤标题党。 二、热点惩罚。对用户在一些热门文章(如前段时间PG One的新闻)上的动作做降权处理。理论上,传播范围较大的内容,置信度会下降。 三、时间衰减。用户兴趣会发生偏移,因此策略更偏向新的用户行为。因此,随着用户动作的增加,老的特征权重会随时间衰减,新动作贡献的特征权重会更大。 四、惩罚展现。如果一篇推荐给用户的文章没有被点击,相关特征(类别,关键词,来源)权重会被惩罚。当 然同时,也要考虑全局背景,是不是相关内容推送比较多,以及相关的关闭和dislike信号等。 用户标签挖掘总体比较简单,主要还是刚刚提到的工程挑战。头条用户标签第一版是批量计算框架,流程比较简单,每天抽取昨天的日活用户过去两个月的动作数据,在Hadoop集群上批量计算结果。 但问题在于,随着用户高速增长,兴趣模型种类和其他批量处理任务都在增加,涉及到的计算量太大。 2014年,批量处理任务几百万用户标签更新的Hadoop任务,当天完成已经开始勉强。集群计算资源紧张很容易影响其它工作,集中写入分布式存储系统的压力也开始增大,并且用户兴趣标签更新延迟越来越高。 面对这些挑战。2014年底今日头条上线了用户标签Storm集群流式计算系统。改成流式之后,只要有用户动作更新就更新标签,CPU代价比较小,可以节省80%的CPU时间,大大降低了计算资源开销。 同时,只需几十台机器就可以支撑每天数千万用户的兴趣模型更新,并且特征更新速度非常快,基本可以做到准实时。这套系统从上线一直使用至今。 当然,我们也发现并非所有用户标签都需要流式系统。像用户的性别、年龄、常驻地点这些信息,不需要实时重复计算,就仍然保留daily更新。 四、评估分析 上面介绍了推荐系统的整体架构,那么如何评估推荐效果好不好? 有一句我认为非常有智慧的话,“一个事情没法评估就没法优化”。对推荐系统也是一样。 事实上,很多因素都会影响推荐效果。比如侯选集合变化,召回模块的改进或增加,推荐特征的增加,模型架构的改进在,算法参数的优化等等,不一一举例。 评估的意义就在于,很多优化最终可能是负向效果,并不是优化上线后效果就会改进。 全面的评估推荐系统,需要完备的评估体系、强大的实验平台以及易用的经验分析工具。 所谓完备的体系就是并非单一指标衡量,不能只看点击率或者停留时长等,需要综合评估。 很多公司算法做的不好,并非是工程师能力不够,而是需要一个强大的实验平台,还有便捷的实验分析工具,可以智能分析数据指标的置信度。 一个良好的评估体系建立需要遵循几个原则,首先是兼顾短期指标与长期指标。我在之前公司负责电商方向的时候观察到,很多策略调整短期内用户觉得新鲜,但是长期看其实没有任何助益。 其次,要兼顾用户指标和生态指标。既要为内容创作者提供价值,让他更有尊严的创作,也有义务满足用户,这两者要平衡。 还有广告主利益也要考虑,这是多方博弈和平衡的过程。 另外,要注意协同效应的影响。实验中严格的流量隔离很难做到,要注意外部效应。 强大的实验平台非常直接的优点是,当同时在线的实验比较多时,可以由平台自动分配流量,无需人工沟通,并且实验结束流量立即回收,提高管理效率。 这能帮助公司降低分析成本,加快算法迭代效应,使整个系统的算法优化工作能够快速往前推进。 这是头条A/B Test实验系统的基本原理。首先我们会做在离线状态下做好用户分桶,然后线上分配实验流量,将桶里用户打上标签,分给实验组。 举个例子,开一个10%流量的实验,两个实验组各5%,一个5%是基线,策略和线上大盘一样,另外一个是新的策略。 实验过程中用户动作会被搜集,基本上是准实时,每小时都可以看到。但因为小时数据有波动,通常是以天为时间节点来看。动作搜集后会有日志处理、分布式统计、写入数据库,非常便捷。 在这个系统下工程师只需要设置流量需求、实验时间、定义特殊过滤条件,自定义实验组ID。系统可以自动生成:实验数据对比、实验数据置信度、实验结论总结以及实验优化建议。 当然,只有实验平台是远远不够的。线上实验平台只能通过数据指标变化推测用户体验的变化,但数据指标和用户体验存在差异,很多指标不能完全量化。 很多改进仍然要通过人工分析,重大改进需要人工评估二次确认。 五、内容安全 最后要介绍今日头条在内容安全上的一些举措。头条现在已经是国内最大的内容创作与分发凭条,必须越来越重视社会责任和行业领导者的责任。如果1%的推荐内容出现问题,就会产生较大的影响。 现在,今日头条的内容主要来源于两部分,一是具有成熟内容生产能力的PGC平台 一是UGC用户内容,如问答、用户评论、微头条。这两部分内容需要通过统一的审核机制。如果是数量相对少的PGC内容,会直接进行风险审核,没有问题会大范围推荐。 UGC内容需要经过一个风险模型的过滤,有问题的会进入二次风险审核。审核通过后,内容会被真正进行推荐。这时如果收到一定量以上的评论或者举报负向反馈,还会再回到复审环节,有问题直接下架。 整个机制相对而言比较健全,作为行业领先者,在内容安全上,今日头条一直用最高的标准要求自己。 分享内容识别技术主要鉴黄模型,谩骂模型以及低俗模型。今日头条的低俗模型通过深度学习算法训练,样本库非常大,图片、文本同时分析。 这部分模型更注重召回率,准确率甚至可以牺牲一些。谩骂模型的样本库同样超过百万,召回率高达95%+,准确率80%+。如果用户经常出言不讳或者不当的评论,我们有一些惩罚机制。 泛低质识别涉及的情况非常多,像假新闻、黑稿、题文不符、标题党、内容质量低等等,这部分内容由机器理解是非常难的,需要大量反馈信息,包括其他样本信息比对。 目前低质模型的准确率和召回率都不是特别高,还需要结合人工复审,将阈值提高。目前最终的召回已达到95%,这部分其实还有非常多的工作可以做。别平台。 如果需要机器学习视频,可以在公众号后台聊天框回复【机器学习】,可以免费获取编程视频 。 你可能还喜欢 数学在机器学习中到底有多重要? AI 新手学习路线,附上最详细的资源整理! 提升机器学习数学基础,推荐7本书 酷爆了!围观2020年十大科技趋势 机器学习该如何入门,听听过来人的经验! 长按加入T圈,接触人工智能 觉得内容还不错的话,给我点个“在看”呗 本篇文章为转载内容。原文链接:https://blog.csdn.net/itcodexy/article/details/109574173。 该文由互联网用户投稿提供,文中观点代表作者本人意见,并不代表本站的立场。 作为信息平台,本站仅提供文章转载服务,并不拥有其所有权,也不对文章内容的真实性、准确性和合法性承担责任。 如发现本文存在侵权、违法、违规或事实不符的情况,请及时联系我们,我们将第一时间进行核实并删除相应内容。
2024-01-13 09:21:23
322
转载
转载文章
...新进展:随着深度学习技术的快速发展,自然语言处理领域也在不断革新。近期,《自然》杂志报道了基于Transformer架构的预训练模型如BERT、RoBERTa等,在中文分词任务上取得的重大突破。通过预训练和微调的方式,这些模型能够在不依赖复杂分词算法的情况下实现高精度的词语切分,并且在长文本理解和语义分析上有显著优势。 2. 开源工具对比及应用场景:除了jieba之外,还有HanLP、LTP(哈工大语言技术平台)等优秀的中文分词开源工具。读者可以通过对比它们在不同场景下的性能表现,了解各自的优缺点以及如何根据实际需求选择合适的分词工具。例如,在处理大规模文本数据集时,考量速度、准确率以及资源消耗等因素至关重要。 3. 行业应用实例剖析:在新闻资讯、搜索引擎优化、社交媒体监控等领域,高效的中文分词技术具有广泛的应用价值。阿里巴巴、腾讯等企业在其产品中就广泛应用了此类技术,用于用户行为分析、智能推荐系统构建等方面。通过研究这些真实案例,可以深入了解jieba等分词工具在解决实际问题时所发挥的关键作用。 4. 学术研究与发展趋势:查阅最新的自然语言处理学术论文,可以发现对于中文分词的研究正逐渐从规则驱动转向数据驱动,并尝试结合多种上下文信息进行更精细化的词语切分。同时,跨语言模型的出现也为中文分词带来了新的挑战与机遇,比如探讨如何利用多语言模型对未登录词或新词进行有效识别和处理。 综上所述,关于jieba中文分词组件的延伸阅读,可以从深度学习技术在分词任务上的前沿发展、同类开源工具比较、具体行业应用案例以及学术研究趋势等多个维度展开,以全面把握这一领域的现状与未来发展方向。
2023-12-02 10:38:37
500
转载
转载文章
...模分布式系统或微服务架构时,通过事件驱动的方式进行组件间通信已成为一种最佳实践。 在实际应用中,.NET Core 3.0引入了源生成器(Source Generators),这一特性使得开发者能够更高效地处理事件和委托,进一步提升代码质量和可维护性。通过自定义源生成器,可以动态创建委托实例并自动绑定相关事件,从而减少手动编写重复代码的工作量。 此外,委托还在并发和多线程编程场景下发挥关键作用,如Task类和async/await关键字背后就依赖于委托来实现异步方法的调用和状态管理。微软在.NET生态系统中提倡采用异步编程模型,利用C的事件和委托机制,能够简化异步操作的处理流程,提高程序性能和响应速度。 对于设计模式层面的理解,委托与观察者模式(Observer Pattern)紧密相连,它允许对象之间的一对多依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知并自动更新。结合最新的.NET技术趋势,诸如Reactive Extensions (Rx.NET)等库更是将这种模式发扬光大,借助LINQ风格的查询操作符和事件流处理,让委托在实时数据流处理领域展现出了强大的功能。 总之,深入掌握C中的委托和事件不仅有助于日常开发工作的效率提升,更能紧跟现代软件工程的发展潮流,充分利用最新的技术和框架优势,构建出高性能、高可维护性的应用程序。而不断跟进官方文档、社区讨论和技术博客,则是深化此类主题理解和实践运用的有效途径。
2023-10-05 16:02:19
80
转载
转载文章
...adata: 元数据name: rs名称 namespace: 所属命名空间 labels: 标签controller: rsspec: 详情描述replicas: 3 副本数量selector: 选择器,通过它指定该控制器管理哪些podmatchLabels: Labels匹配规则app: nginx-podmatchExpressions: Expressions匹配规则,key就是label的key,values的值是个数组,意思是标签值必须是此数组中的其中一个才能匹配上;- {key: app, operator: In, values: [nginx-pod]}template: 模板,当副本数量不足时,会根据下面的模板创建pod副本metadata:labels: 这里的标签必须和上面的matchLabels一致,将他们关联起来app: nginx-podspec:containers:- name: nginximage: nginx:1.17.1ports:- containerPort: 80 1、创建一个ReplicaSet 新建一个文件 rs.yaml,内容如下 apiVersion: apps/v1kind: ReplicaSet pod控制器metadata: 元数据name: pc-replicaset 名字namespace: dev 名称空间spec:replicas: 3 副本数selector: 选择器,通过它指定该控制器管理哪些podmatchLabels: Labels匹配规则app: nginx-podtemplate: 模板,当副本数量不足时,会根据下面的模板创建pod副本metadata:labels:app: nginx-podspec:containers:- name: nginximage: nginx:1.17.1 运行 kubectl create -f rs.yaml 获取replicaset kubectl get replicaset -n dev 2、扩缩容 刚刚我们已经用第一种方式创建了一个replicaSet,现在就基于原来的rs进行扩容,原来的副本数量是3个,现在我们将其扩到6个,做法也很简单,运行编辑命令 第一种方式: scale 使用scale命令实现扩缩容,后面--replicas=n直接指定目标数量即可kubectl scale rs pc-replicaset --replicas=2 -n dev 第二种方式:使用edit命令编辑rs 这种方式相当于使用vi编辑修改yaml配置的内容,进去后将replicas的值改为1,保存后自动生效kubectl edit rs pc-replicaset -n dev 3、镜像版本变更 第一种方式:scale kubectl scale rs pc-replicaset nginx=nginx:1.71.2 -n dev 第二种方式:edit 这种方式相当于使用vi编辑修改yaml配置的内容,进去后将nginx的值改为nginx:1.71.2,保存后自动生效kubectl edit rs pc-replicaset -n dev 4、删除rs 第一种方式kubectl delete -f rs.yaml 第二种方式 ,如果想要只删rs,但不删除pod,可在删除时加上--cascade=false参数(不推荐)kubectl delete rs pc-replicaset -n dev --cascade=false 2、Deployment k8s v1.2版本后加入Deployment;这种控制器不直接控制pod,而是通过管理ReplicaSet来间接管理pod;也就是Deployment管理ReplicaSet,ReplicaSet管理pod;所以 Deployment 比 ReplicaSet 功能更加强大 当我们创建了一个Deployment之后,也会自动创建一个ReplicaSet 功能 支持ReplicaSet 的所有功能 支持发布的停止、继续 支持版本的滚动更新和回退功能 配置模板 新建文件 apiVersion: apps/v1 版本号kind: Deployment 类型 metadata: 元数据name: rs名称 namespace: 所属命名空间 labels: 标签controller: deployspec: 详情描述replicas: 3 副本数量revisionHistoryLimit: 3 保留历史版本的数量,默认10,内部通过保留rs来实现paused: false 暂停部署,默认是falseprogressDeadlineSeconds: 600 部署超时时间(s),默认是600strategy: 策略type: RollingUpdate 滚动更新策略rollingUpdate: 滚动更新maxSurge: 30% 最大额外可以存在的副本数,可以为百分比,也可以为整数maxUnavailable: 30% 最大不可用状态的 Pod 的最大值,可以为百分比,也可以为整数selector: 选择器,通过它指定该控制器管理哪些podmatchLabels: Labels匹配规则app: nginx-podmatchExpressions: Expressions匹配规则- {key: app, operator: In, values: [nginx-pod]}template: 模板,当副本数量不足时,会根据下面的模板创建pod副本metadata:labels:app: nginx-podspec:containers:- name: nginximage: nginx:1.17.1ports:- containerPort: 80 1、创建和删除Deployment 创建pc-deployment.yaml,内容如下: apiVersion: apps/v1kind: Deployment metadata:name: pc-deploymentnamespace: devspec: replicas: 3selector:matchLabels:app: nginx-podtemplate:metadata:labels:app: nginx-podspec:containers:- name: nginximage: nginx:1.17.1 创建和查看 创建deployment,--record=true 表示记录整个deployment更新过程[root@k8s-master01 ~] kubectl create -f pc-deployment.yaml --record=truedeployment.apps/pc-deployment created 查看deployment READY 可用的/总数 UP-TO-DATE 最新版本的pod的数量 AVAILABLE 当前可用的pod的数量[root@k8s-master01 ~] kubectl get deploy pc-deployment -n devNAME READY UP-TO-DATE AVAILABLE AGEpc-deployment 3/3 3 3 15s 查看rs 发现rs的名称是在原来deployment的名字后面添加了一个10位数的随机串[root@k8s-master01 ~] kubectl get rs -n devNAME DESIRED CURRENT READY AGEpc-deployment-6696798b78 3 3 3 23s 查看pod[root@k8s-master01 ~] kubectl get pods -n devNAME READY STATUS RESTARTS AGEpc-deployment-6696798b78-d2c8n 1/1 Running 0 107spc-deployment-6696798b78-smpvp 1/1 Running 0 107spc-deployment-6696798b78-wvjd8 1/1 Running 0 107s 删除deployment 删除deployment,其下的rs和pod也将被删除kubectl delete -f pc-deployment.yaml 2、扩缩容 deployment的扩缩容和 ReplicaSet 的扩缩容一样,只需要将rs或者replicaSet改为deployment即可,具体请参考上面的 ReplicaSet 扩缩容 3、镜像更新 刚刚在创建时加上了--record=true参数,所以在一旦进行了镜像更新,就会新建出一个pod出来,将老的old-pod上的容器全删除,然后在新的new-pod上在新建对应数量的容器,此时old-pod是不会删除的,因为这个old-pod是要进行回退的; 镜像更新策略有2种 滚动更新(RollingUpdate):(默认值),杀死一部分,就启动一部分,在更新过程中,存在两个版本Pod 重建更新(Recreate):在创建出新的Pod之前会先杀掉所有已存在的Pod strategy:指定新的Pod替换旧的Pod的策略, 支持两个属性:type:指定策略类型,支持两种策略Recreate:在创建出新的Pod之前会先杀掉所有已存在的PodRollingUpdate:滚动更新,就是杀死一部分,就启动一部分,在更新过程中,存在两个版本PodrollingUpdate:当type为RollingUpdate时生效,用于为RollingUpdate设置参数,支持两个属性:maxUnavailable:用来指定在升级过程中不可用Pod的最大数量,默认为25%。maxSurge: 用来指定在升级过程中可以超过期望的Pod的最大数量,默认为25%。 重建更新 编辑pc-deployment.yaml,在spec节点下添加更新策略 spec:strategy: 策略type: Recreate 重建更新 创建deploy进行验证 变更镜像[root@k8s-master01 ~] kubectl set image deployment pc-deployment nginx=nginx:1.17.2 -n devdeployment.apps/pc-deployment image updated 观察升级过程[root@k8s-master01 ~] kubectl get pods -n dev -wNAME READY STATUS RESTARTS AGEpc-deployment-5d89bdfbf9-65qcw 1/1 Running 0 31spc-deployment-5d89bdfbf9-w5nzv 1/1 Running 0 31spc-deployment-5d89bdfbf9-xpt7w 1/1 Running 0 31spc-deployment-5d89bdfbf9-xpt7w 1/1 Terminating 0 41spc-deployment-5d89bdfbf9-65qcw 1/1 Terminating 0 41spc-deployment-5d89bdfbf9-w5nzv 1/1 Terminating 0 41spc-deployment-675d469f8b-grn8z 0/1 Pending 0 0spc-deployment-675d469f8b-hbl4v 0/1 Pending 0 0spc-deployment-675d469f8b-67nz2 0/1 Pending 0 0spc-deployment-675d469f8b-grn8z 0/1 ContainerCreating 0 0spc-deployment-675d469f8b-hbl4v 0/1 ContainerCreating 0 0spc-deployment-675d469f8b-67nz2 0/1 ContainerCreating 0 0spc-deployment-675d469f8b-grn8z 1/1 Running 0 1spc-deployment-675d469f8b-67nz2 1/1 Running 0 1spc-deployment-675d469f8b-hbl4v 1/1 Running 0 2s 滚动更新 编辑pc-deployment.yaml,在spec节点下添加更新策略 spec:strategy: 策略type: RollingUpdate 滚动更新策略rollingUpdate:maxSurge: 25% maxUnavailable: 25% 创建deploy进行验证 变更镜像[root@k8s-master01 ~] kubectl set image deployment pc-deployment nginx=nginx:1.17.3 -n dev deployment.apps/pc-deployment image updated 观察升级过程[root@k8s-master01 ~] kubectl get pods -n dev -wNAME READY STATUS RESTARTS AGEpc-deployment-c848d767-8rbzt 1/1 Running 0 31mpc-deployment-c848d767-h4p68 1/1 Running 0 31mpc-deployment-c848d767-hlmz4 1/1 Running 0 31mpc-deployment-c848d767-rrqcn 1/1 Running 0 31mpc-deployment-966bf7f44-226rx 0/1 Pending 0 0spc-deployment-966bf7f44-226rx 0/1 ContainerCreating 0 0spc-deployment-966bf7f44-226rx 1/1 Running 0 1spc-deployment-c848d767-h4p68 0/1 Terminating 0 34mpc-deployment-966bf7f44-cnd44 0/1 Pending 0 0spc-deployment-966bf7f44-cnd44 0/1 ContainerCreating 0 0spc-deployment-966bf7f44-cnd44 1/1 Running 0 2spc-deployment-c848d767-hlmz4 0/1 Terminating 0 34mpc-deployment-966bf7f44-px48p 0/1 Pending 0 0spc-deployment-966bf7f44-px48p 0/1 ContainerCreating 0 0spc-deployment-966bf7f44-px48p 1/1 Running 0 0spc-deployment-c848d767-8rbzt 0/1 Terminating 0 34mpc-deployment-966bf7f44-dkmqp 0/1 Pending 0 0spc-deployment-966bf7f44-dkmqp 0/1 ContainerCreating 0 0spc-deployment-966bf7f44-dkmqp 1/1 Running 0 2spc-deployment-c848d767-rrqcn 0/1 Terminating 0 34m 至此,新版本的pod创建完毕,就版本的pod销毁完毕 中间过程是滚动进行的,也就是边销毁边创建 4、版本回退 更新 刚刚在创建时加上了--record=true参数,所以在一旦进行了镜像更新,就会新建出一个pod出来,将老的old-pod上的容器全删除,然后在新的new-pod上在新建对应数量的容器,此时old-pod是不会删除的,因为这个old-pod是要进行回退的; 回退 在回退时会将new-pod上的容器全部删除,在将old-pod上恢复原来的容器; 回退命令 kubectl rollout: 版本升级相关功能,支持下面的选项: status 显示当前升级状态 history 显示 升级历史记录 pause 暂停版本升级过程 resume 继续已经暂停的版本升级过程 restart 重启版本升级过程 undo 回滚到上一级版本(可以使用–to-revision回滚到指定版本) 用法 查看当前升级版本的状态kubectl rollout status deploy pc-deployment -n dev 查看升级历史记录kubectl rollout history deploy pc-deployment -n dev 版本回滚 这里直接使用--to-revision=1回滚到了1版本, 如果省略这个选项,就是回退到上个版本kubectl rollout undo deployment pc-deployment --to-revision=1 -n dev 金丝雀发布 Deployment控制器支持控制更新过程中的控制,如“暂停(pause)”或“继续(resume)”更新操作。 比如有一批新的Pod资源创建完成后立即暂停更新过程,此时,仅存在一部分新版本的应用,主体部分还是旧的版本。然后,再筛选一小部分的用户请求路由到新版本的Pod应用,继续观察能否稳定地按期望的方式运行。确定没问题之后再继续完成余下的Pod资源滚动更新,否则立即回滚更新操作。这就是所谓的金丝雀发布。 金丝雀发布不是自动完成的,需要人为手动去操作,才能达到金丝雀发布的标准; 更新deployment的版本,并配置暂停deploymentkubectl set image deploy pc-deployment nginx=nginx:1.17.4 -n dev && kubectl rollout pause deployment pc-deployment -n dev 观察更新状态kubectl rollout status deploy pc-deployment -n dev 监控更新的过程kubectl get rs -n dev -o wide 确保更新的pod没问题了,继续更新kubectl rollout resume deploy pc-deployment -n dev 如果有问题,就回退到上个版本回退到上个版本kubectl rollout undo deployment pc-deployment -n dev Horizontal Pod Autoscaler 简称HPA,使用deployment可以手动调整pod的数量来实现扩容和缩容;但是这显然不符合k8s的自动化的定位,k8s期望可以通过检测pod的使用情况,实现pod数量自动调整,于是就有了HPA控制器; HPA可以获取每个Pod利用率,然后和HPA中定义的指标进行对比,同时计算出需要伸缩的具体值,最后实现Pod的数量的调整。比如说我指定了一个规则:当我的cpu利用率达到90%或者内存使用率到达80%的时候,就需要进行调整pod的副本数量,每次添加n个pod副本; 其实HPA与之前的Deployment一样,也属于一种Kubernetes资源对象,它通过追踪分析ReplicaSet控制器的所有目标Pod的负载变化情况,来确定是否需要针对性地调整目标Pod的副本数,也就是HPA管理Deployment,Deployment管理ReplicaSet,ReplicaSet管理pod,这是HPA的实现原理。 1、安装metrics-server metrics-server可以用来收集集群中的资源使用情况 安装git[root@k8s-master01 ~] yum install git -y 获取metrics-server, 注意使用的版本[root@k8s-master01 ~] git clone -b v0.3.6 https://github.com/kubernetes-incubator/metrics-server 修改deployment, 注意修改的是镜像和初始化参数[root@k8s-master01 ~] cd /root/metrics-server/deploy/1.8+/[root@k8s-master01 1.8+] vim metrics-server-deployment.yaml按图中添加下面选项hostNetwork: trueimage: registry.cn-hangzhou.aliyuncs.com/google_containers/metrics-server-amd64:v0.3.6args:- --kubelet-insecure-tls- --kubelet-preferred-address-types=InternalIP,Hostname,InternalDNS,ExternalDNS,ExternalIP 2、安装metrics-server [root@k8s-master01 1.8+] kubectl apply -f ./ 3、查看pod运行情况 [root@k8s-master01 1.8+] kubectl get pod -n kube-systemmetrics-server-6b976979db-2xwbj 1/1 Running 0 90s 4、使用kubectl top node 查看资源使用情况 [root@k8s-master01 1.8+] kubectl top nodeNAME CPU(cores) CPU% MEMORY(bytes) MEMORY%k8s-master01 289m 14% 1582Mi 54% k8s-node01 81m 4% 1195Mi 40% k8s-node02 72m 3% 1211Mi 41% [root@k8s-master01 1.8+] kubectl top pod -n kube-systemNAME CPU(cores) MEMORY(bytes)coredns-6955765f44-7ptsb 3m 9Micoredns-6955765f44-vcwr5 3m 8Mietcd-master 14m 145Mi... 至此,metrics-server安装完成 5、 准备deployment和servie 创建pc-hpa-pod.yaml文件,内容如下: apiVersion: apps/v1kind: Deploymentmetadata:name: nginxnamespace: devspec:strategy: 策略type: RollingUpdate 滚动更新策略replicas: 1selector:matchLabels:app: nginx-podtemplate:metadata:labels:app: nginx-podspec:containers:- name: nginximage: nginx:1.17.1resources: 资源配额limits: 限制资源(上限)cpu: "1" CPU限制,单位是core数requests: 请求资源(下限)cpu: "100m" CPU限制,单位是core数 创建deployment [root@k8s-master01 1.8+] kubectl run nginx --image=nginx:1.17.1 --requests=cpu=100m -n dev 6、创建service [root@k8s-master01 1.8+] kubectl expose deployment nginx --type=NodePort --port=80 -n dev 7、查看 [root@k8s-master01 1.8+] kubectl get deployment,pod,svc -n devNAME READY UP-TO-DATE AVAILABLE AGEdeployment.apps/nginx 1/1 1 1 47sNAME READY STATUS RESTARTS AGEpod/nginx-7df9756ccc-bh8dr 1/1 Running 0 47sNAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGEservice/nginx NodePort 10.101.18.29 <none> 80:31830/TCP 35s 8、 部署HPA 创建pc-hpa.yaml文件,内容如下: apiVersion: autoscaling/v1kind: HorizontalPodAutoscalermetadata:name: pc-hpanamespace: devspec:minReplicas: 1 最小pod数量maxReplicas: 10 最大pod数量 ,pod数量会在1~10之间自动伸缩targetCPUUtilizationPercentage: 3 CPU使用率指标,如果cpu使用率达到3%就会进行扩容;为了测试方便,将这个数值调小一些scaleTargetRef: 指定要控制的nginx信息apiVersion: /v1kind: Deploymentname: nginx 创建hpa [root@k8s-master01 1.8+] kubectl create -f pc-hpa.yamlhorizontalpodautoscaler.autoscaling/pc-hpa created 查看hpa [root@k8s-master01 1.8+] kubectl get hpa -n devNAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGEpc-hpa Deployment/nginx 0%/3% 1 10 1 62s 9、 测试 使用压测工具对service地址192.168.5.4:31830进行压测,然后通过控制台查看hpa和pod的变化 hpa变化 [root@k8s-master01 ~] kubectl get hpa -n dev -wNAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGEpc-hpa Deployment/nginx 0%/3% 1 10 1 4m11spc-hpa Deployment/nginx 0%/3% 1 10 1 5m19spc-hpa Deployment/nginx 22%/3% 1 10 1 6m50spc-hpa Deployment/nginx 22%/3% 1 10 4 7m5spc-hpa Deployment/nginx 22%/3% 1 10 8 7m21spc-hpa Deployment/nginx 6%/3% 1 10 8 7m51spc-hpa Deployment/nginx 0%/3% 1 10 8 9m6spc-hpa Deployment/nginx 0%/3% 1 10 8 13mpc-hpa Deployment/nginx 0%/3% 1 10 1 14m deployment变化 [root@k8s-master01 ~] kubectl get deployment -n dev -wNAME READY UP-TO-DATE AVAILABLE AGEnginx 1/1 1 1 11mnginx 1/4 1 1 13mnginx 1/4 1 1 13mnginx 1/4 1 1 13mnginx 1/4 4 1 13mnginx 1/8 4 1 14mnginx 1/8 4 1 14mnginx 1/8 4 1 14mnginx 1/8 8 1 14mnginx 2/8 8 2 14mnginx 3/8 8 3 14mnginx 4/8 8 4 14mnginx 5/8 8 5 14mnginx 6/8 8 6 14mnginx 7/8 8 7 14mnginx 8/8 8 8 15mnginx 8/1 8 8 20mnginx 8/1 8 8 20mnginx 1/1 1 1 20m pod变化 [root@k8s-master01 ~] kubectl get pods -n dev -wNAME READY STATUS RESTARTS AGEnginx-7df9756ccc-bh8dr 1/1 Running 0 11mnginx-7df9756ccc-cpgrv 0/1 Pending 0 0snginx-7df9756ccc-8zhwk 0/1 Pending 0 0snginx-7df9756ccc-rr9bn 0/1 Pending 0 0snginx-7df9756ccc-cpgrv 0/1 ContainerCreating 0 0snginx-7df9756ccc-8zhwk 0/1 ContainerCreating 0 0snginx-7df9756ccc-rr9bn 0/1 ContainerCreating 0 0snginx-7df9756ccc-m9gsj 0/1 Pending 0 0snginx-7df9756ccc-g56qb 0/1 Pending 0 0snginx-7df9756ccc-sl9c6 0/1 Pending 0 0snginx-7df9756ccc-fgst7 0/1 Pending 0 0snginx-7df9756ccc-g56qb 0/1 ContainerCreating 0 0snginx-7df9756ccc-m9gsj 0/1 ContainerCreating 0 0snginx-7df9756ccc-sl9c6 0/1 ContainerCreating 0 0snginx-7df9756ccc-fgst7 0/1 ContainerCreating 0 0snginx-7df9756ccc-8zhwk 1/1 Running 0 19snginx-7df9756ccc-rr9bn 1/1 Running 0 30snginx-7df9756ccc-m9gsj 1/1 Running 0 21snginx-7df9756ccc-cpgrv 1/1 Running 0 47snginx-7df9756ccc-sl9c6 1/1 Running 0 33snginx-7df9756ccc-g56qb 1/1 Running 0 48snginx-7df9756ccc-fgst7 1/1 Running 0 66snginx-7df9756ccc-fgst7 1/1 Terminating 0 6m50snginx-7df9756ccc-8zhwk 1/1 Terminating 0 7m5snginx-7df9756ccc-cpgrv 1/1 Terminating 0 7m5snginx-7df9756ccc-g56qb 1/1 Terminating 0 6m50snginx-7df9756ccc-rr9bn 1/1 Terminating 0 7m5snginx-7df9756ccc-m9gsj 1/1 Terminating 0 6m50snginx-7df9756ccc-sl9c6 1/1 Terminating 0 6m50s DaemonSet 简称DS,ds可以保证在集群中的每一台节点(或指定节点)上都运行一个副本,一般适用于日志收集、节点监控等场景;也就是说,如果一个Pod提供的功能是节点级别的(每个节点都需要且只需要一个),那么这类Pod就适合使用DaemonSet类型的控制器创建。 DaemonSet控制器的特点: 每当向集群中添加一个节点时,指定的 Pod 副本也将添加到该节点上 当节点从集群中移除时,Pod 也就被垃圾回收了 配置模板 apiVersion: apps/v1 版本号kind: DaemonSet 类型 metadata: 元数据name: rs名称 namespace: 所属命名空间 labels: 标签controller: daemonsetspec: 详情描述revisionHistoryLimit: 3 保留历史版本updateStrategy: 更新策略type: RollingUpdate 滚动更新策略rollingUpdate: 滚动更新maxUnavailable: 1 最大不可用状态的 Pod 的最大值,可以为百分比,也可以为整数selector: 选择器,通过它指定该控制器管理哪些podmatchLabels: Labels匹配规则app: nginx-podmatchExpressions: Expressions匹配规则- {key: app, operator: In, values: [nginx-pod]}template: 模板,当副本数量不足时,会根据下面的模板创建pod副本metadata:labels:app: nginx-podspec:containers:- name: nginximage: nginx:1.17.1ports:- containerPort: 80 1、创建ds 创建pc-daemonset.yaml,内容如下: apiVersion: apps/v1kind: DaemonSet metadata:name: pc-daemonsetnamespace: devspec: selector:matchLabels:app: nginx-podtemplate:metadata:labels:app: nginx-podspec:containers:- name: nginximage: nginx:1.17.1 运行 创建daemonset[root@k8s-master01 ~] kubectl create -f pc-daemonset.yamldaemonset.apps/pc-daemonset created 查看daemonset[root@k8s-master01 ~] kubectl get ds -n dev -o wideNAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES pc-daemonset 2 2 2 2 2 24s nginx nginx:1.17.1 查看pod,发现在每个Node上都运行一个pod[root@k8s-master01 ~] kubectl get pods -n dev -o wideNAME READY STATUS RESTARTS AGE IP NODE pc-daemonset-9bck8 1/1 Running 0 37s 10.244.1.43 node1 pc-daemonset-k224w 1/1 Running 0 37s 10.244.2.74 node2 2、删除daemonset [root@k8s-master01 ~] kubectl delete -f pc-daemonset.yamldaemonset.apps "pc-daemonset" deleted Job 主要用于负责批量处理一次性(每个任务仅运行一次就结束)任务。当然,你也可以运行多次,配置好即可,Job特点如下: 当Job创建的pod执行成功结束时,Job将记录成功结束的pod数量 当成功结束的pod达到指定的数量时,Job将完成执行 配置模板 apiVersion: batch/v1 版本号kind: Job 类型 metadata: 元数据name: rs名称 namespace: 所属命名空间 labels: 标签controller: jobspec: 详情描述completions: 1 指定job需要成功运行Pods的次数。默认值: 1parallelism: 1 指定job在任一时刻应该并发运行Pods的数量。默认值: 1activeDeadlineSeconds: 30 指定job可运行的时间期限,超过时间还未结束,系统将会尝试进行终止。backoffLimit: 6 指定job失败后进行重试的次数。默认是6manualSelector: true 是否可以使用selector选择器选择pod,默认是falseselector: 选择器,通过它指定该控制器管理哪些podmatchLabels: Labels匹配规则app: counter-podmatchExpressions: Expressions匹配规则- {key: app, operator: In, values: [counter-pod]}template: 模板,当副本数量不足时,会根据下面的模板创建pod副本metadata:labels:app: counter-podspec:restartPolicy: Never 重启策略只能设置为Never或者OnFailurecontainers:- name: counterimage: busybox:1.30command: ["bin/sh","-c","for i in 9 8 7 6 5 4 3 2 1; do echo $i;sleep 2;done"] 关于重启策略设置的说明:(这里只能设置为Never或者OnFailure) 如果指定为OnFailure,则job会在pod出现故障时重启容器,而不是创建pod,failed次数不变 如果指定为Never,则job会在pod出现故障时创建新的pod,并且故障pod不会消失,也不会重启,failed次数加1 如果指定为Always的话,就意味着一直重启,意味着job任务会重复去执行了,当然不对,所以不能设置为Always 1、创建一个job 创建pc-job.yaml,内容如下: apiVersion: batch/v1kind: Job metadata:name: pc-jobnamespace: devspec:manualSelector: trueselector:matchLabels:app: counter-podtemplate:metadata:labels:app: counter-podspec:restartPolicy: Nevercontainers:- name: counterimage: busybox:1.30command: ["bin/sh","-c","for i in 9 8 7 6 5 4 3 2 1; do echo $i;sleep 3;done"] 创建 创建job[root@k8s-master01 ~] kubectl create -f pc-job.yamljob.batch/pc-job created 查看job[root@k8s-master01 ~] kubectl get job -n dev -o wide -wNAME COMPLETIONS DURATION AGE CONTAINERS IMAGES SELECTORpc-job 0/1 21s 21s counter busybox:1.30 app=counter-podpc-job 1/1 31s 79s counter busybox:1.30 app=counter-pod 通过观察pod状态可以看到,pod在运行完毕任务后,就会变成Completed状态[root@k8s-master01 ~] kubectl get pods -n dev -wNAME READY STATUS RESTARTS AGEpc-job-rxg96 1/1 Running 0 29spc-job-rxg96 0/1 Completed 0 33s 接下来,调整下pod运行的总数量和并行数量 即:在spec下设置下面两个选项 completions: 6 指定job需要成功运行Pods的次数为6 parallelism: 3 指定job并发运行Pods的数量为3 然后重新运行job,观察效果,此时会发现,job会每次运行3个pod,总共执行了6个pod[root@k8s-master01 ~] kubectl get pods -n dev -wNAME READY STATUS RESTARTS AGEpc-job-684ft 1/1 Running 0 5spc-job-jhj49 1/1 Running 0 5spc-job-pfcvh 1/1 Running 0 5spc-job-684ft 0/1 Completed 0 11spc-job-v7rhr 0/1 Pending 0 0spc-job-v7rhr 0/1 Pending 0 0spc-job-v7rhr 0/1 ContainerCreating 0 0spc-job-jhj49 0/1 Completed 0 11spc-job-fhwf7 0/1 Pending 0 0spc-job-fhwf7 0/1 Pending 0 0spc-job-pfcvh 0/1 Completed 0 11spc-job-5vg2j 0/1 Pending 0 0spc-job-fhwf7 0/1 ContainerCreating 0 0spc-job-5vg2j 0/1 Pending 0 0spc-job-5vg2j 0/1 ContainerCreating 0 0spc-job-fhwf7 1/1 Running 0 2spc-job-v7rhr 1/1 Running 0 2spc-job-5vg2j 1/1 Running 0 3spc-job-fhwf7 0/1 Completed 0 12spc-job-v7rhr 0/1 Completed 0 12spc-job-5vg2j 0/1 Completed 0 12s 2、删除 删除jobkubectl delete -f pc-job.yaml CronJob 简称为CJ,CronJob控制器以 Job控制器资源为其管控对象,并借助它管理pod资源对象,Job控制器定义的作业任务在其控制器资源创建之后便会立即执行,但CronJob可以以类似于Linux操作系统的周期性任务作业计划的方式控制其运行时间点及重复运行的方式。也就是说,CronJob可以在特定的时间点(反复的)去运行job任务。可以理解为定时任务 配置模板 apiVersion: batch/v1beta1 版本号kind: CronJob 类型 metadata: 元数据name: rs名称 namespace: 所属命名空间 labels: 标签controller: cronjobspec: 详情描述schedule: cron格式的作业调度运行时间点,用于控制任务在什么时间执行concurrencyPolicy: 并发执行策略,用于定义前一次作业运行尚未完成时是否以及如何运行后一次的作业failedJobHistoryLimit: 为失败的任务执行保留的历史记录数,默认为1successfulJobHistoryLimit: 为成功的任务执行保留的历史记录数,默认为3startingDeadlineSeconds: 启动作业错误的超时时长jobTemplate: job控制器模板,用于为cronjob控制器生成job对象;下面其实就是job的定义metadata:spec:completions: 1parallelism: 1activeDeadlineSeconds: 30backoffLimit: 6manualSelector: trueselector:matchLabels:app: counter-podmatchExpressions: 规则- {key: app, operator: In, values: [counter-pod]}template:metadata:labels:app: counter-podspec:restartPolicy: Never containers:- name: counterimage: busybox:1.30command: ["bin/sh","-c","for i in 9 8 7 6 5 4 3 2 1; do echo $i;sleep 20;done"] cron表达式写法 需要重点解释的几个选项:schedule: cron表达式,用于指定任务的执行时间/1 <分钟> <小时> <日> <月份> <星期>分钟 值从 0 到 59.小时 值从 0 到 23.日 值从 1 到 31.月 值从 1 到 12.星期 值从 0 到 6, 0 代表星期日多个时间可以用逗号隔开; 范围可以用连字符给出;可以作为通配符; /表示每... 例如1 // 每个小时的第一分钟执行/1 // 每分钟都执行concurrencyPolicy:Allow: 允许Jobs并发运行(默认)Forbid: 禁止并发运行,如果上一次运行尚未完成,则跳过下一次运行Replace: 替换,取消当前正在运行的作业并用新作业替换它 1、创建cronJob 创建pc-cronjob.yaml,内容如下: apiVersion: batch/v1beta1kind: CronJobmetadata:name: pc-cronjobnamespace: devlabels:controller: cronjobspec:schedule: "/1 " 每分钟执行一次jobTemplate:metadata:spec:template:spec:restartPolicy: Nevercontainers:- name: counterimage: busybox:1.30command: ["bin/sh","-c","for i in 9 8 7 6 5 4 3 2 1; do echo $i;sleep 3;done"] 运行 创建cronjob[root@k8s-master01 ~] kubectl create -f pc-cronjob.yamlcronjob.batch/pc-cronjob created 查看cronjob[root@k8s-master01 ~] kubectl get cronjobs -n devNAME SCHEDULE SUSPEND ACTIVE LAST SCHEDULE AGEpc-cronjob /1 False 0 <none> 6s 查看job[root@k8s-master01 ~] kubectl get jobs -n devNAME COMPLETIONS DURATION AGEpc-cronjob-1592587800 1/1 28s 3m26spc-cronjob-1592587860 1/1 28s 2m26spc-cronjob-1592587920 1/1 28s 86s 查看pod[root@k8s-master01 ~] kubectl get pods -n devpc-cronjob-1592587800-x4tsm 0/1 Completed 0 2m24spc-cronjob-1592587860-r5gv4 0/1 Completed 0 84spc-cronjob-1592587920-9dxxq 1/1 Running 0 24s 2、删除cronjob kubectl delete -f pc-cronjob.yaml pod调度 什么是调度 默认情况下,一个pod在哪个node节点上运行,是通过scheduler组件采用相应的算法计算出来的,这个过程是不受人工控制的; 调度规则 但是在实际使用中,我们想控制某些pod定向到达某个节点上,应该怎么做呢?其实k8s提供了四类调度规则 调度方式 描述 自动调度 通过scheduler组件采用相应的算法计算得出运行在哪个节点上 定向调度 运行到指定的node节点上,通过NodeName、NodeSelector实现 亲和性调度 跟谁关系好就调度到哪个节点上 1、nodeAffinity :节点亲和性,调度到关系好的节点上 2、podAffinity:pod亲和性,调度到关系好的pod所在的节点上 3、PodAntAffinity:pod反清河行,调度到关系差的那个pod所在的节点上 污点(容忍)调度 污点是站在node的角度上的,比如果nodeA有一个污点,大家都别来,此时nodeA会拒绝master调度过来的pod 定向调度 指的是利用在pod上声明nodeName或nodeSelector的方式将pod调度到指定的pod节点上,因为这种定向调度是强制性的,所以如果node节点不存在的话,也会向上面进行调度,只不过pod会运行失败; 1、定向调度-> nodeName nodeName 是将pod强制调度到指定名称的node节点上,这种方式跳过了scheduler的调度逻辑,直接将pod调度到指定名称的节点上,配置文件内容如下 apiVersion: v1 版本号kind: Pod 资源类型metadata: name: pod-namenamespace: devspec: containers: - image: nginx:1.17.1name: nginx-containernodeName: node1 调度到node1节点上 2、定向调度 -> NodeSelector NodeSelector是将pod调度到添加了指定label标签的node节点上,它是通过k8s的label-selector机制实现的,也就是说,在创建pod之前,会由scheduler用matchNodeSelecto调度策略进行label标签的匹配,找出目标node,然后在将pod调度到目标node; 要实验NodeSelector,首先得给node节点加上label标签 kubectl label nodes node1 nodetag=node1 配置文件内容如下 apiVersion: v1 版本号kind: Pod 资源类型metadata: name: pod-namenamespace: devspec: containers: - image: nginx:1.17.1name: nginx-containernodeSelector: nodetag: node1 调度到具有nodetag=node1标签的节点上 本篇文章为转载内容。原文链接:https://blog.csdn.net/qq_27184497/article/details/121765387。 该文由互联网用户投稿提供,文中观点代表作者本人意见,并不代表本站的立场。 作为信息平台,本站仅提供文章转载服务,并不拥有其所有权,也不对文章内容的真实性、准确性和合法性承担责任。 如发现本文存在侵权、违法、违规或事实不符的情况,请及时联系我们,我们将第一时间进行核实并删除相应内容。
2023-09-29 09:08:28
422
转载
转载文章
...综合案例 需求分析和技术架构 广告点击系统实时分析 广告来自于广告或者移动App等,广告需要设定在具体的广告位,当用户点击广告的时候,一般都会通过ajax或Socket往后台发送日志数据,在这里我们是要做基于SparkStreaming做实时在线统计。那么数据就需要放进消息系统(Kafka)中,我们的Spark Streaming应用程序就会去Kafka中Pull数据过来进行计算和消费,并把计算后的数据放入到持久化系统中(MySQL) 广告点击系统实时分析的意义:因为可以在线实时的看见广告的投放效果,就为广告的更大规模的投入和调整打下了坚实的基础,从而为公司带来最大化的经济回报。 核心需求: 1、实时黑名单动态过滤出有效的用户广告点击行为:因为黑名单用户可能随时出现,所以需要动态更新; 2、在线计算广告点击流量; 3、Top3热门广告; 4、每个广告流量趋势; 5、广告点击用户的区域分布分析 6、最近一分钟的广告点击量; 7、整个广告点击Spark Streaming处理程序724小时运行; 数据格式: 时间、用户、广告、城市等 技术细节: 在线计算用户点击的次数分析,屏蔽IP等; 使用updateStateByKey或者mapWithState进行不同地区广告点击排名的计算; Spark Streaming+Spark SQL+Spark Core等综合分析数据; 使用Window类型的操作; 高可用和性能调优等等; 流量趋势,一般会结合DB等; Spark Core / /package com.tom.spark.SparkApps.sparkstreaming;import java.util.Date;import java.util.HashMap;import java.util.Map;import java.util.Properties;import java.util.Random;import kafka.javaapi.producer.Producer;import kafka.producer.KeyedMessage;import kafka.producer.ProducerConfig;/ 数据生成代码,Kafka Producer产生数据/public class MockAdClickedStat {/ @param args/public static void main(String[] args) {final Random random = new Random();final String[] provinces = new String[]{"Guangdong", "Zhejiang", "Jiangsu", "Fujian"};final Map<String, String[]> cities = new HashMap<String, String[]>();cities.put("Guangdong", new String[]{"Guangzhou", "Shenzhen", "Dongguan"});cities.put("Zhejiang", new String[]{"Hangzhou", "Wenzhou", "Ningbo"});cities.put("Jiangsu", new String[]{"Nanjing", "Suzhou", "Wuxi"});cities.put("Fujian", new String[]{"Fuzhou", "Xiamen", "Sanming"});final String[] ips = new String[] {"192.168.112.240","192.168.112.239","192.168.112.245","192.168.112.246","192.168.112.247","192.168.112.248","192.168.112.249","192.168.112.250","192.168.112.251","192.168.112.252","192.168.112.253","192.168.112.254",};/ Kafka相关的基本配置信息/Properties kafkaConf = new Properties();kafkaConf.put("serializer.class", "kafka.serializer.StringEncoder");kafkaConf.put("metadeta.broker.list", "Master:9092,Worker1:9092,Worker2:9092");ProducerConfig producerConfig = new ProducerConfig(kafkaConf);final Producer<Integer, String> producer = new Producer<Integer, String>(producerConfig);new Thread(new Runnable() {public void run() {while(true) {//在线处理广告点击流的基本数据格式:timestamp、ip、userID、adID、province、cityLong timestamp = new Date().getTime();String ip = ips[random.nextInt(12)]; //可以采用网络上免费提供的ip库int userID = random.nextInt(10000);int adID = random.nextInt(100);String province = provinces[random.nextInt(4)];String city = cities.get(province)[random.nextInt(3)];String clickedAd = timestamp + "\t" + ip + "\t" + userID + "\t" + adID + "\t" + province + "\t" + city;producer.send(new KeyedMessage<Integer, String>("AdClicked", clickedAd));try {Thread.sleep(50);} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();} }} }).start();} } package com.tom.spark.SparkApps.sparkstreaming;import java.sql.Connection;import java.sql.DriverManager;import java.sql.PreparedStatement;import java.sql.ResultSet;import java.sql.SQLException;import java.util.ArrayList;import java.util.Arrays;import java.util.HashMap;import java.util.HashSet;import java.util.Iterator;import java.util.List;import java.util.Map;import java.util.Set;import java.util.concurrent.LinkedBlockingQueue;import kafka.serializer.StringDecoder;import org.apache.spark.SparkConf;import org.apache.spark.api.java.JavaPairRDD;import org.apache.spark.api.java.JavaRDD;import org.apache.spark.api.java.JavaSparkContext;import org.apache.spark.api.java.function.Function;import org.apache.spark.api.java.function.Function2;import org.apache.spark.api.java.function.PairFunction;import org.apache.spark.api.java.function.VoidFunction;import org.apache.spark.sql.DataFrame;import org.apache.spark.sql.Row;import org.apache.spark.sql.RowFactory;import org.apache.spark.sql.hive.HiveContext;import org.apache.spark.sql.types.DataTypes;import org.apache.spark.sql.types.StructType;import org.apache.spark.streaming.Durations;import org.apache.spark.streaming.api.java.JavaDStream;import org.apache.spark.streaming.api.java.JavaPairDStream;import org.apache.spark.streaming.api.java.JavaPairInputDStream;import org.apache.spark.streaming.api.java.JavaStreamingContext;import org.apache.spark.streaming.api.java.JavaStreamingContextFactory;import org.apache.spark.streaming.kafka.KafkaUtils;import com.google.common.base.Optional;import scala.Tuple2;/ 数据处理,Kafka消费者/public class AdClickedStreamingStats {/ @param args/public static void main(String[] args) {// TODO Auto-generated method stub//好处:1、checkpoint 2、工厂final SparkConf conf = new SparkConf().setAppName("SparkStreamingOnKafkaDirect").setMaster("hdfs://Master:7077/");final String checkpointDirectory = "hdfs://Master:9000/library/SparkStreaming/CheckPoint_Data";JavaStreamingContextFactory factory = new JavaStreamingContextFactory() {public JavaStreamingContext create() {// TODO Auto-generated method stubreturn createContext(checkpointDirectory, conf);} };/ 可以从失败中恢复Driver,不过还需要指定Driver这个进程运行在Cluster,并且在提交应用程序的时候制定--supervise;/JavaStreamingContext javassc = JavaStreamingContext.getOrCreate(checkpointDirectory, factory);/ 第三步:创建Spark Streaming输入数据来源input Stream: 1、数据输入来源可以基于File、HDFS、Flume、Kafka、Socket等 2、在这里我们指定数据来源于网络Socket端口,Spark Streaming连接上该端口并在运行的时候一直监听该端口的数据 (当然该端口服务首先必须存在),并且在后续会根据业务需要不断有数据产生(当然对于Spark Streaming 应用程序的运行而言,有无数据其处理流程都是一样的) 3、如果经常在每间隔5秒钟没有数据的话不断启动空的Job其实会造成调度资源的浪费,因为并没有数据需要发生计算;所以 实际的企业级生成环境的代码在具体提交Job前会判断是否有数据,如果没有的话就不再提交Job;///创建Kafka元数据来让Spark Streaming这个Kafka Consumer利用Map<String, String> kafkaParameters = new HashMap<String, String>();kafkaParameters.put("metadata.broker.list", "Master:9092,Worker1:9092,Worker2:9092");Set<String> topics = new HashSet<String>();topics.add("SparkStreamingDirected");JavaPairInputDStream<String, String> adClickedStreaming = KafkaUtils.createDirectStream(javassc, String.class, String.class, StringDecoder.class, StringDecoder.class,kafkaParameters, topics);/因为要对黑名单进行过滤,而数据是在RDD中的,所以必然使用transform这个函数; 但是在这里我们必须使用transformToPair,原因是读取进来的Kafka的数据是Pair<String,String>类型, 另一个原因是过滤后的数据要进行进一步处理,所以必须是读进的Kafka数据的原始类型 在此再次说明,每个Batch Duration中实际上讲输入的数据就是被一个且仅被一个RDD封装的,你可以有多个 InputDStream,但其实在产生job的时候,这些不同的InputDStream在Batch Duration中就相当于Spark基于HDFS 数据操作的不同文件来源而已罢了。/JavaPairDStream<String, String> filteredadClickedStreaming = adClickedStreaming.transformToPair(new Function<JavaPairRDD<String,String>, JavaPairRDD<String,String>>() {public JavaPairRDD<String, String> call(JavaPairRDD<String, String> rdd) throws Exception {/ 在线黑名单过滤思路步骤: 1、从数据库中获取黑名单转换成RDD,即新的RDD实例封装黑名单数据; 2、然后把代表黑名单的RDD的实例和Batch Duration产生的RDD进行Join操作, 准确的说是进行leftOuterJoin操作,也就是说使用Batch Duration产生的RDD和代表黑名单的RDD实例进行 leftOuterJoin操作,如果两者都有内容的话,就会是true,否则的话就是false 我们要留下的是leftOuterJoin结果为false; /final List<String> blackListNames = new ArrayList<String>();JDBCWrapper jdbcWrapper = JDBCWrapper.getJDBCInstance();jdbcWrapper.doQuery("SELECT FROM blacklisttable", null, new ExecuteCallBack() {public void resultCallBack(ResultSet result) throws Exception {while(result.next()){blackListNames.add(result.getString(1));} }});List<Tuple2<String, Boolean>> blackListTuple = new ArrayList<Tuple2<String,Boolean>>();for(String name : blackListNames) {blackListTuple.add(new Tuple2<String, Boolean>(name, true));}List<Tuple2<String, Boolean>> blacklistFromListDB = blackListTuple; //数据来自于查询的黑名单表并且映射成为<String, Boolean>JavaSparkContext jsc = new JavaSparkContext(rdd.context());/ 黑名单的表中只有userID,但是如果要进行join操作的话就必须是Key-Value,所以在这里我们需要 基于数据表中的数据产生Key-Value类型的数据集合/JavaPairRDD<String, Boolean> blackListRDD = jsc.parallelizePairs(blacklistFromListDB);/ 进行操作的时候肯定是基于userID进行join,所以必须把传入的rdd进行mapToPair操作转化成为符合格式的RDD/JavaPairRDD<String, Tuple2<String, String>> rdd2Pair = rdd.mapToPair(new PairFunction<Tuple2<String,String>, String, Tuple2<String, String>>() {public Tuple2<String, Tuple2<String, String>> call(Tuple2<String, String> t) throws Exception {// TODO Auto-generated method stubString userID = t._2.split("\t")[2];return new Tuple2<String, Tuple2<String,String>>(userID, t);} });JavaPairRDD<String, Tuple2<Tuple2<String, String>, Optional<Boolean>>> joined = rdd2Pair.leftOuterJoin(blackListRDD);JavaPairRDD<String, String> result = joined.filter(new Function<Tuple2<String,Tuple2<Tuple2<String,String>,Optional<Boolean>>>, Boolean>() {public Boolean call(Tuple2<String, Tuple2<Tuple2<String, String>, Optional<Boolean>>> tuple)throws Exception {// TODO Auto-generated method stubOptional<Boolean> optional = tuple._2._2;if(optional.isPresent() && optional.get()){return false;} else {return true;} }}).mapToPair(new PairFunction<Tuple2<String,Tuple2<Tuple2<String,String>,Optional<Boolean>>>, String, String>() {public Tuple2<String, String> call(Tuple2<String, Tuple2<Tuple2<String, String>, Optional<Boolean>>> t)throws Exception {// TODO Auto-generated method stubreturn t._2._1;} });return result;} });//广告点击的基本数据格式:timestamp、ip、userID、adID、province、cityJavaPairDStream<String, Long> pairs = filteredadClickedStreaming.mapToPair(new PairFunction<Tuple2<String,String>, String, Long>() {public Tuple2<String, Long> call(Tuple2<String, String> t) throws Exception {String[] splited=t._2.split("\t");String timestamp = splited[0]; //YYYY-MM-DDString ip = splited[1];String userID = splited[2];String adID = splited[3];String province = splited[4];String city = splited[5]; String clickedRecord = timestamp + "_" +ip + "_"+userID+"_"+adID+"_"+province +"_"+city;return new Tuple2<String, Long>(clickedRecord, 1L);} });/ 第4.3步:在单词实例计数为1基础上,统计每个单词在文件中出现的总次数/JavaPairDStream<String, Long> adClickedUsers= pairs.reduceByKey(new Function2<Long, Long, Long>() {public Long call(Long i1, Long i2) throws Exception{return i1 + i2;} });/判断有效的点击,复杂化的采用机器学习训练模型进行在线过滤 简单的根据ip判断1天不超过100次;也可以通过一个batch duration的点击次数判断是否非法广告点击,通过一个batch来判断是不完整的,还需要一天的数据也可以每一个小时来判断。/JavaPairDStream<String, Long> filterClickedBatch = adClickedUsers.filter(new Function<Tuple2<String,Long>, Boolean>() {public Boolean call(Tuple2<String, Long> v1) throws Exception {if (1 < v1._2){//更新一些黑名单的数据库表return false;} else { return true;} }});//filterClickedBatch.print();//写入数据库filterClickedBatch.foreachRDD(new Function<JavaPairRDD<String,Long>, Void>() {public Void call(JavaPairRDD<String, Long> rdd) throws Exception {rdd.foreachPartition(new VoidFunction<Iterator<Tuple2<String,Long>>>() {public void call(Iterator<Tuple2<String, Long>> partition) throws Exception {//使用数据库连接池的高效读写数据库的方式将数据写入数据库mysql//例如一次插入 1000条 records,使用insertBatch 或 updateBatch//插入的用户数据信息:userID,adID,clickedCount,time//这里面有一个问题,可能出现两条记录的key是一样的,此时需要更新累加操作List<UserAdClicked> userAdClickedList = new ArrayList<UserAdClicked>();while(partition.hasNext()) {Tuple2<String, Long> record = partition.next();String[] splited = record._1.split("\t");UserAdClicked userClicked = new UserAdClicked();userClicked.setTimestamp(splited[0]);userClicked.setIp(splited[1]);userClicked.setUserID(splited[2]);userClicked.setAdID(splited[3]);userClicked.setProvince(splited[4]);userClicked.setCity(splited[5]);userAdClickedList.add(userClicked);}final List<UserAdClicked> inserting = new ArrayList<UserAdClicked>();final List<UserAdClicked> updating = new ArrayList<UserAdClicked>();JDBCWrapper jdbcWrapper = JDBCWrapper.getJDBCInstance();//表的字段timestamp、ip、userID、adID、province、city、clickedCountfor(final UserAdClicked clicked : userAdClickedList) {jdbcWrapper.doQuery("SELECT clickedCount FROM adclicked WHERE"+ " timestamp =? AND userID = ? AND adID = ?",new Object[]{clicked.getTimestamp(), clicked.getUserID(),clicked.getAdID()}, new ExecuteCallBack() {public void resultCallBack(ResultSet result) throws Exception {// TODO Auto-generated method stubif(result.next()) {long count = result.getLong(1);clicked.setClickedCount(count);updating.add(clicked);} else {inserting.add(clicked);clicked.setClickedCount(1L);} }});}//表的字段timestamp、ip、userID、adID、province、city、clickedCountList<Object[]> insertParametersList = new ArrayList<Object[]>();for(UserAdClicked insertRecord : inserting) {insertParametersList.add(new Object[] {insertRecord.getTimestamp(),insertRecord.getIp(),insertRecord.getUserID(),insertRecord.getAdID(),insertRecord.getProvince(),insertRecord.getCity(),insertRecord.getClickedCount()});}jdbcWrapper.doBatch("INSERT INTO adclicked VALUES(?, ?, ?, ?, ?, ?, ?)", insertParametersList);//表的字段timestamp、ip、userID、adID、province、city、clickedCountList<Object[]> updateParametersList = new ArrayList<Object[]>();for(UserAdClicked updateRecord : updating) {updateParametersList.add(new Object[] {updateRecord.getTimestamp(),updateRecord.getIp(),updateRecord.getUserID(),updateRecord.getAdID(),updateRecord.getProvince(),updateRecord.getCity(),updateRecord.getClickedCount() + 1});}jdbcWrapper.doBatch("UPDATE adclicked SET clickedCount = ? WHERE"+ " timestamp =? AND ip = ? AND userID = ? AND adID = ? "+ "AND province = ? AND city = ?", updateParametersList);} });return null;} });//再次过滤,从数据库中读取数据过滤黑名单JavaPairDStream<String, Long> blackListBasedOnHistory = filterClickedBatch.filter(new Function<Tuple2<String,Long>, Boolean>() {public Boolean call(Tuple2<String, Long> v1) throws Exception {//广告点击的基本数据格式:timestamp,ip,userID,adID,province,cityString[] splited = v1._1.split("\t"); //提取key值String date =splited[0];String userID =splited[2];String adID =splited[3];//查询一下数据库同一个用户同一个广告id点击量超过50次列入黑名单//接下来 根据date、userID、adID条件去查询用户点击广告的数据表,获得总的点击次数//这个时候基于点击次数判断是否属于黑名单点击int clickedCountTotalToday = 81 ;if (clickedCountTotalToday > 50) {return true;}else {return false ;} }});//map操作,找出用户的idJavaDStream<String> blackListuserIDBasedInBatchOnhistroy =blackListBasedOnHistory.map(new Function<Tuple2<String,Long>, String>() {public String call(Tuple2<String, Long> v1) throws Exception {// TODO Auto-generated method stubreturn v1._1.split("\t")[2];} });//有一个问题,数据可能重复,在一个partition里面重复,这个好办;//但多个partition不能保证一个用户重复,需要对黑名单的整个rdd进行去重操作。//rdd去重了,partition也就去重了,一石二鸟,一箭双雕// 找出了黑名单,下一步就写入黑名单数据库表中JavaDStream<String> blackListUniqueuserBasedInBatchOnhistroy = blackListuserIDBasedInBatchOnhistroy.transform(new Function<JavaRDD<String>, JavaRDD<String>>() {public JavaRDD<String> call(JavaRDD<String> rdd) throws Exception {// TODO Auto-generated method stubreturn rdd.distinct();} });// 下一步写入到数据表中blackListUniqueuserBasedInBatchOnhistroy.foreachRDD(new Function<JavaRDD<String>, Void>() {public Void call(JavaRDD<String> rdd) throws Exception {rdd.foreachPartition(new VoidFunction<Iterator<String>>() {public void call(Iterator<String> t) throws Exception {// TODO Auto-generated method stub//插入的用户信息可以只包含:useID//此时直接插入黑名单数据表即可。//写入数据库List<Object[]> blackList = new ArrayList<Object[]>();while(t.hasNext()) {blackList.add(new Object[]{t.next()});}JDBCWrapper jdbcWrapper = JDBCWrapper.getJDBCInstance();jdbcWrapper.doBatch("INSERT INTO blacklisttable values (?)", blackList);} });return null;} });/广告点击累计动态更新,每个updateStateByKey都会在Batch Duration的时间间隔的基础上进行广告点击次数的更新, 更新之后我们一般都会持久化到外部存储设备上,在这里我们存储到MySQL数据库中/JavaPairDStream<String, Long> updateStateByKeyDSteam = filteredadClickedStreaming.mapToPair(new PairFunction<Tuple2<String,String>, String, Long>() {public Tuple2<String, Long> call(Tuple2<String, String> t)throws Exception {String[] splited=t._2.split("\t");String timestamp = splited[0]; //YYYY-MM-DDString ip = splited[1];String userID = splited[2];String adID = splited[3];String province = splited[4];String city = splited[5]; String clickedRecord = timestamp + "_" +ip + "_"+userID+"_"+adID+"_"+province +"_"+city;return new Tuple2<String, Long>(clickedRecord, 1L);} }).updateStateByKey(new Function2<List<Long>, Optional<Long>, Optional<Long>>() {public Optional<Long> call(List<Long> v1, Optional<Long> v2)throws Exception {// v1:当前的Key在当前的Batch Duration中出现的次数的集合,例如{1,1,1,。。。,1}// v2:当前的Key在以前的Batch Duration中积累下来的结果;Long clickedTotalHistory = 0L; if(v2.isPresent()){clickedTotalHistory = v2.get();}for(Long one : v1) {clickedTotalHistory += one;}return Optional.of(clickedTotalHistory);} });updateStateByKeyDSteam.foreachRDD(new Function<JavaPairRDD<String,Long>, Void>() {public Void call(JavaPairRDD<String, Long> rdd) throws Exception {rdd.foreachPartition(new VoidFunction<Iterator<Tuple2<String,Long>>>() {public void call(Iterator<Tuple2<String, Long>> partition) throws Exception {//使用数据库连接池的高效读写数据库的方式将数据写入数据库mysql//例如一次插入 1000条 records,使用insertBatch 或 updateBatch//插入的用户数据信息:timestamp、adID、province、city//这里面有一个问题,可能出现两条记录的key是一样的,此时需要更新累加操作List<AdClicked> AdClickedList = new ArrayList<AdClicked>();while(partition.hasNext()) {Tuple2<String, Long> record = partition.next();String[] splited = record._1.split("\t");AdClicked adClicked = new AdClicked();adClicked.setTimestamp(splited[0]);adClicked.setAdID(splited[1]);adClicked.setProvince(splited[2]);adClicked.setCity(splited[3]);adClicked.setClickedCount(record._2);AdClickedList.add(adClicked);}final List<AdClicked> inserting = new ArrayList<AdClicked>();final List<AdClicked> updating = new ArrayList<AdClicked>();JDBCWrapper jdbcWrapper = JDBCWrapper.getJDBCInstance();//表的字段timestamp、ip、userID、adID、province、city、clickedCountfor(final AdClicked clicked : AdClickedList) {jdbcWrapper.doQuery("SELECT clickedCount FROM adclickedcount WHERE"+ " timestamp = ? AND adID = ? AND province = ? AND city = ?",new Object[]{clicked.getTimestamp(), clicked.getAdID(),clicked.getProvince(), clicked.getCity()}, new ExecuteCallBack() {public void resultCallBack(ResultSet result) throws Exception {// TODO Auto-generated method stubif(result.next()) {long count = result.getLong(1);clicked.setClickedCount(count);updating.add(clicked);} else {inserting.add(clicked);clicked.setClickedCount(1L);} }});}//表的字段timestamp、ip、userID、adID、province、city、clickedCountList<Object[]> insertParametersList = new ArrayList<Object[]>();for(AdClicked insertRecord : inserting) {insertParametersList.add(new Object[] {insertRecord.getTimestamp(),insertRecord.getAdID(),insertRecord.getProvince(),insertRecord.getCity(),insertRecord.getClickedCount()});}jdbcWrapper.doBatch("INSERT INTO adclickedcount VALUES(?, ?, ?, ?, ?)", insertParametersList);//表的字段timestamp、ip、userID、adID、province、city、clickedCountList<Object[]> updateParametersList = new ArrayList<Object[]>();for(AdClicked updateRecord : updating) {updateParametersList.add(new Object[] {updateRecord.getClickedCount(),updateRecord.getTimestamp(),updateRecord.getAdID(),updateRecord.getProvince(),updateRecord.getCity()});}jdbcWrapper.doBatch("UPDATE adclickedcount SET clickedCount = ? WHERE"+ " timestamp =? AND adID = ? AND province = ? AND city = ?", updateParametersList);} });return null;} });/ 对广告点击进行TopN计算,计算出每天每个省份Top5排名的广告 因为我们直接对RDD进行操作,所以使用了transfomr算子;/updateStateByKeyDSteam.transform(new Function<JavaPairRDD<String,Long>, JavaRDD<Row>>() {public JavaRDD<Row> call(JavaPairRDD<String, Long> rdd) throws Exception {JavaRDD<Row> rowRDD = rdd.mapToPair(new PairFunction<Tuple2<String,Long>, String, Long>() {public Tuple2<String, Long> call(Tuple2<String, Long> t)throws Exception {// TODO Auto-generated method stubString[] splited=t._1.split("_");String timestamp = splited[0]; //YYYY-MM-DDString adID = splited[3];String province = splited[4];String clickedRecord = timestamp + "_" + adID + "_" + province;return new Tuple2<String, Long>(clickedRecord, t._2);} }).reduceByKey(new Function2<Long, Long, Long>() {public Long call(Long v1, Long v2) throws Exception {// TODO Auto-generated method stubreturn v1 + v2;} }).map(new Function<Tuple2<String,Long>, Row>() {public Row call(Tuple2<String, Long> v1) throws Exception {// TODO Auto-generated method stubString[] splited=v1._1.split("_");String timestamp = splited[0]; //YYYY-MM-DDString adID = splited[3];String province = splited[4];return RowFactory.create(timestamp, adID, province, v1._2);} });StructType structType = DataTypes.createStructType(Arrays.asList(DataTypes.createStructField("timestamp", DataTypes.StringType, true),DataTypes.createStructField("adID", DataTypes.StringType, true),DataTypes.createStructField("province", DataTypes.StringType, true),DataTypes.createStructField("clickedCount", DataTypes.LongType, true)));HiveContext hiveContext = new HiveContext(rdd.context());DataFrame df = hiveContext.createDataFrame(rowRDD, structType);df.registerTempTable("topNTableSource");DataFrame result = hiveContext.sql("SELECT timestamp, adID, province, clickedCount, FROM"+ " (SELECT timestamp, adID, province,clickedCount, "+ "ROW_NUMBER() OVER(PARTITION BY province ORDER BY clickeCount DESC) rank "+ "FROM topNTableSource) subquery "+ "WHERE rank <= 5");return result.toJavaRDD();} }).foreachRDD(new Function<JavaRDD<Row>, Void>() {public Void call(JavaRDD<Row> rdd) throws Exception {// TODO Auto-generated method stubrdd.foreachPartition(new VoidFunction<Iterator<Row>>() {public void call(Iterator<Row> t) throws Exception {// TODO Auto-generated method stubList<AdProvinceTopN> adProvinceTopN = new ArrayList<AdProvinceTopN>();while(t.hasNext()) {Row row = t.next();AdProvinceTopN item = new AdProvinceTopN();item.setTimestamp(row.getString(0));item.setAdID(row.getString(1));item.setProvince(row.getString(2));item.setClickedCount(row.getLong(3));adProvinceTopN.add(item);}// final List<AdProvinceTopN> inserting = new ArrayList<AdProvinceTopN>();// final List<AdProvinceTopN> updating = new ArrayList<AdProvinceTopN>();JDBCWrapper jdbcWrapper = JDBCWrapper.getJDBCInstance();Set<String> set = new HashSet<String>();for(AdProvinceTopN item: adProvinceTopN){set.add(item.getTimestamp() + "_" + item.getProvince());}//表的字段timestamp、adID、province、clickedCountArrayList<Object[]> deleteParametersList = new ArrayList<Object[]>();for(String deleteRecord : set) {String[] splited = deleteRecord.split("_");deleteParametersList.add(new Object[]{splited[0],splited[1]});}jdbcWrapper.doBatch("DELETE FROM adprovincetopn WHERE timestamp = ? AND province = ?", deleteParametersList);//表的字段timestamp、ip、userID、adID、province、city、clickedCountList<Object[]> insertParametersList = new ArrayList<Object[]>();for(AdProvinceTopN insertRecord : adProvinceTopN) {insertParametersList.add(new Object[] {insertRecord.getClickedCount(),insertRecord.getTimestamp(),insertRecord.getAdID(),insertRecord.getProvince()});}jdbcWrapper.doBatch("INSERT INTO adprovincetopn VALUES (?, ?, ?, ?)", insertParametersList);} });return null;} });/ 计算过去半个小时内广告点击的趋势 广告点击的基本数据格式:timestamp、ip、userID、adID、province、city/filteredadClickedStreaming.mapToPair(new PairFunction<Tuple2<String,String>, String, Long>() {public Tuple2<String, Long> call(Tuple2<String, String> t)throws Exception {String splited[] = t._2.split("\t");String adID = splited[3];String time = splited[0]; //Todo:后续需要重构代码实现时间戳和分钟的转换提取。此处需要提取出该广告的点击分钟单位return new Tuple2<String, Long>(time + "_" + adID, 1L);} }).reduceByKeyAndWindow(new Function2<Long, Long, Long>() {public Long call(Long v1, Long v2) throws Exception {// TODO Auto-generated method stubreturn v1 + v2;} }, new Function2<Long, Long, Long>() {public Long call(Long v1, Long v2) throws Exception {// TODO Auto-generated method stubreturn v1 - v2;} }, Durations.minutes(30), Durations.milliseconds(5)).foreachRDD(new Function<JavaPairRDD<String,Long>, Void>() {public Void call(JavaPairRDD<String, Long> rdd) throws Exception {// TODO Auto-generated method stubrdd.foreachPartition(new VoidFunction<Iterator<Tuple2<String,Long>>>() {public void call(Iterator<Tuple2<String, Long>> partition)throws Exception {List<AdTrendStat> adTrend = new ArrayList<AdTrendStat>();// TODO Auto-generated method stubwhile(partition.hasNext()) {Tuple2<String, Long> record = partition.next();String[] splited = record._1.split("_");String time = splited[0];String adID = splited[1];Long clickedCount = record._2;/ 在插入数据到数据库的时候具体需要哪些字段?time、adID、clickedCount; 而我们通过J2EE技术进行趋势绘图的时候肯定是需要年、月、日、时、分这个维度的,所以我们在这里需要 年月日、小时、分钟这些时间维度;/AdTrendStat adTrendStat = new AdTrendStat();adTrendStat.setAdID(adID);adTrendStat.setClickedCount(clickedCount);adTrendStat.set_date(time); //Todo:获取年月日adTrendStat.set_hour(time); //Todo:获取小时adTrendStat.set_minute(time);//Todo:获取分钟adTrend.add(adTrendStat);}final List<AdTrendStat> inserting = new ArrayList<AdTrendStat>();final List<AdTrendStat> updating = new ArrayList<AdTrendStat>();JDBCWrapper jdbcWrapper = JDBCWrapper.getJDBCInstance();//表的字段timestamp、ip、userID、adID、province、city、clickedCountfor(final AdTrendStat trend : adTrend) {final AdTrendCountHistory adTrendhistory = new AdTrendCountHistory();jdbcWrapper.doQuery("SELECT clickedCount FROM adclickedtrend WHERE"+ " date =? AND hour = ? AND minute = ? AND AdID = ?",new Object[]{trend.get_date(), trend.get_hour(), trend.get_minute(),trend.getAdID()}, new ExecuteCallBack() {public void resultCallBack(ResultSet result) throws Exception {// TODO Auto-generated method stubif(result.next()) {long count = result.getLong(1);adTrendhistory.setClickedCountHistoryLong(count);updating.add(trend);} else { inserting.add(trend);} }});}//表的字段date、hour、minute、adID、clickedCountList<Object[]> insertParametersList = new ArrayList<Object[]>();for(AdTrendStat insertRecord : inserting) {insertParametersList.add(new Object[] {insertRecord.get_date(),insertRecord.get_hour(),insertRecord.get_minute(),insertRecord.getAdID(),insertRecord.getClickedCount()});}jdbcWrapper.doBatch("INSERT INTO adclickedtrend VALUES(?, ?, ?, ?, ?)", insertParametersList);//表的字段date、hour、minute、adID、clickedCountList<Object[]> updateParametersList = new ArrayList<Object[]>();for(AdTrendStat updateRecord : updating) {updateParametersList.add(new Object[] {updateRecord.getClickedCount(),updateRecord.get_date(),updateRecord.get_hour(),updateRecord.get_minute(),updateRecord.getAdID()});}jdbcWrapper.doBatch("UPDATE adclickedtrend SET clickedCount = ? WHERE"+ " date =? AND hour = ? AND minute = ? AND AdID = ?", updateParametersList);} });return null;} });;/ Spark Streaming 执行引擎也就是Driver开始运行,Driver启动的时候是位于一条新的线程中的,当然其内部有消息循环体,用于 接收应用程序本身或者Executor中的消息,/javassc.start();javassc.awaitTermination();javassc.close();}private static JavaStreamingContext createContext(String checkpointDirectory, SparkConf conf) {// If you do not see this printed, that means the StreamingContext has been loaded// from the new checkpointSystem.out.println("Creating new context");// Create the context with a 5 second batch sizeJavaStreamingContext ssc = new JavaStreamingContext(conf, Durations.seconds(10));ssc.checkpoint(checkpointDirectory);return ssc;} }class JDBCWrapper {private static JDBCWrapper jdbcInstance = null;private static LinkedBlockingQueue<Connection> dbConnectionPool = new LinkedBlockingQueue<Connection>();static {try {Class.forName("com.mysql.jdbc.Driver");} catch (ClassNotFoundException e) {// TODO Auto-generated catch blocke.printStackTrace();} }public static JDBCWrapper getJDBCInstance() {if(jdbcInstance == null) {synchronized (JDBCWrapper.class) {if(jdbcInstance == null) {jdbcInstance = new JDBCWrapper();} }}return jdbcInstance; }private JDBCWrapper() {for(int i = 0; i < 10; i++){try {Connection conn = DriverManager.getConnection("jdbc:mysql://Master:3306/sparkstreaming","root", "root");dbConnectionPool.put(conn);} catch (Exception e) {// TODO Auto-generated catch blocke.printStackTrace();} } }public synchronized Connection getConnection() {while(0 == dbConnectionPool.size()){try {Thread.sleep(20);} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();} }return dbConnectionPool.poll();}public int[] doBatch(String sqlText, List<Object[]> paramsList){Connection conn = getConnection();PreparedStatement preparedStatement = null;int[] result = null;try {conn.setAutoCommit(false);preparedStatement = conn.prepareStatement(sqlText);for(Object[] parameters: paramsList) {for(int i = 0; i < parameters.length; i++){preparedStatement.setObject(i + 1, parameters[i]);} preparedStatement.addBatch();}result = preparedStatement.executeBatch();conn.commit();} catch (SQLException e) {// TODO Auto-generated catch blocke.printStackTrace();} finally {if(preparedStatement != null) {try {preparedStatement.close();} catch (SQLException e) {// TODO Auto-generated catch blocke.printStackTrace();} }if(conn != null) {try {dbConnectionPool.put(conn);} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();} }}return result; }public void doQuery(String sqlText, Object[] paramsList, ExecuteCallBack callback){Connection conn = getConnection();PreparedStatement preparedStatement = null;ResultSet result = null;try {preparedStatement = conn.prepareStatement(sqlText);for(int i = 0; i < paramsList.length; i++){preparedStatement.setObject(i + 1, paramsList[i]);} result = preparedStatement.executeQuery();try {callback.resultCallBack(result);} catch (Exception e) {// TODO Auto-generated catch blocke.printStackTrace();} } catch (SQLException e) {// TODO Auto-generated catch blocke.printStackTrace();} finally {if(preparedStatement != null) {try {preparedStatement.close();} catch (SQLException e) {// TODO Auto-generated catch blocke.printStackTrace();} }if(conn != null) {try {dbConnectionPool.put(conn);} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();} }} }}interface ExecuteCallBack {void resultCallBack(ResultSet result) throws Exception;}class UserAdClicked {private String timestamp;private String ip;private String userID;private String adID;private String province;private String city;private Long clickedCount;public String getTimestamp() {return timestamp;}public void setTimestamp(String timestamp) {this.timestamp = timestamp;}public String getIp() {return ip;}public void setIp(String ip) {this.ip = ip;}public String getUserID() {return userID;}public void setUserID(String userID) {this.userID = userID;}public String getAdID() {return adID;}public void setAdID(String adID) {this.adID = adID;}public String getProvince() {return province;}public void setProvince(String province) {this.province = province;}public String getCity() {return city;}public void setCity(String city) {this.city = city;}public Long getClickedCount() {return clickedCount;}public void setClickedCount(Long clickedCount) {this.clickedCount = clickedCount;} }class AdClicked {private String timestamp;private String adID;private String province;private String city;private Long clickedCount;public String getTimestamp() {return timestamp;}public void setTimestamp(String timestamp) {this.timestamp = timestamp;}public String getAdID() {return adID;}public void setAdID(String adID) {this.adID = adID;}public String getProvince() {return province;}public void setProvince(String province) {this.province = province;}public String getCity() {return city;}public void setCity(String city) {this.city = city;}public Long getClickedCount() {return clickedCount;}public void setClickedCount(Long clickedCount) {this.clickedCount = clickedCount;} }class AdProvinceTopN {private String timestamp;private String adID;private String province;private Long clickedCount;public String getTimestamp() {return timestamp;}public void setTimestamp(String timestamp) {this.timestamp = timestamp;}public String getAdID() {return adID;}public void setAdID(String adID) {this.adID = adID;}public String getProvince() {return province;}public void setProvince(String province) {this.province = province;}public Long getClickedCount() {return clickedCount;}public void setClickedCount(Long clickedCount) {this.clickedCount = clickedCount;} }class AdTrendStat {private String _date;private String _hour;private String _minute;private String adID;private Long clickedCount;public String get_date() {return _date;}public void set_date(String _date) {this._date = _date;}public String get_hour() {return _hour;}public void set_hour(String _hour) {this._hour = _hour;}public String get_minute() {return _minute;}public void set_minute(String _minute) {this._minute = _minute;}public String getAdID() {return adID;}public void setAdID(String adID) {this.adID = adID;}public Long getClickedCount() {return clickedCount;}public void setClickedCount(Long clickedCount) {this.clickedCount = clickedCount;} }class AdTrendCountHistory{private Long clickedCountHistoryLong;public Long getClickedCountHistoryLong() {return clickedCountHistoryLong;}public void setClickedCountHistoryLong(Long clickedCountHistoryLong) {this.clickedCountHistoryLong = clickedCountHistoryLong;} } 本篇文章为转载内容。原文链接:https://blog.csdn.net/tom_8899_li/article/details/71194434。 该文由互联网用户投稿提供,文中观点代表作者本人意见,并不代表本站的立场。 作为信息平台,本站仅提供文章转载服务,并不拥有其所有权,也不对文章内容的真实性、准确性和合法性承担责任。 如发现本文存在侵权、违法、违规或事实不符的情况,请及时联系我们,我们将第一时间进行核实并删除相应内容。
2023-02-14 19:16:35
297
转载
转载文章
...、高并发场景和分布式架构中具有广泛的应用价值。为了紧跟技术发展动态并进一步探讨其实际应用场景,以下是一些延伸阅读的推荐内容: 1. 最新实践案例:近期,某知名电商平台在其秒杀活动系统中采用Redis的发布订阅模式优化了库存扣减与订单创建流程,确保了数据一致性的同时显著提升了系统吞吐量。深入分析这一案例,我们可以学习如何在实际项目中结合使用Redis的多种特性来解决复杂业务问题。 2. 技术深度解析:“Redis 6.2版本对事务和Lua脚本执行机制的改进”——随着Redis新版本的迭代更新,其对事务处理和Lua脚本的支持更加完善,比如新增的多线程支持大幅提高了Lua脚本执行性能,同时针对事务模型也进行了增强,以更好地满足高并发环境下的需求。 3. 行业发展趋势:“基于Redis构建微服务架构中的事件驱动系统”——文章讨论了在微服务架构中如何利用Redis的发布订阅模式构建事件驱动的服务间通信机制,并辅以具体实例阐述了这种方式如何提升系统的响应速度与可扩展性。 4. 学术研究视角:“从CAP理论角度看Redis在分布式系统中的作用”——学术界针对Redis在分布式系统中的角色进行了深度剖析,尤其是针对消息队列和发布订阅模式在满足CAP定理中的权衡问题,为开发者提供了理论指导和实践启示。 5. 实用教程分享:“利用Lua脚本实现Redis高级功能实战指南”——一些技术博客和社区发布了系列教程,详细介绍了如何编写高效安全的Lua脚本来处理复杂的Redis操作,如自定义原子操作、限流控制等,是广大开发者进阶Redis应用能力的实用参考资源。
2024-03-18 12:25:04
541
转载
转载文章
...T Identity技术,描述了如何运用ASP.NET Identity实现应用程序的用户管理,以及实现应用程序的认证与授权等相关技术,译者希望本系列教程能成为掌握ASP.NET Identity技术的一份完整而有价值的资料。读者若是能够按照文章的描述,一边阅读、一边实践、一边理解,定能有意想不到的巨大收获!希望本系列博文能够得到广大园友的高度推荐。 15 Advanced ASP.NET Identity 15 ASP.NET Identity高级技术 In this chapter, I finish my description of ASP.NET Identity by showing you some of the advanced features it offers. I demonstrate how you can extend the database schema by defining custom properties on the user class and how to use database migrations to apply those properties without deleting the data in the ASP.NET Identity database. I also explain how ASP.NET Identity supports the concept of claims and demonstrates how they can be used to flexibly authorize access to action methods. I finish the chapter—and the book—by showing you how ASP.NET Identity makes it easy to authenticate users through third parties. I demonstrate authentication with Google accounts, but ASP.NET Identity has built-in support for Microsoft, Facebook, and Twitter accounts as well. Table 15-1 summarizes this chapter. 本章将完成对ASP.NET Identity的描述,向你展示它所提供的一些高级特性。我将演示,你可以扩展ASP.NET Identity的数据库架构,其办法是在用户类上定义一些自定义属性。也会演示如何使用数据库迁移,这样可以运用自定义属性,而不必删除ASP.NET Identity数据库中的数据。还会解释ASP.NET Identity如何支持声明(Claims)概念,并演示如何将它们灵活地用来对动作方法进行授权访问。最后向你展示ASP.NET Identity很容易通过第三方部件来认证用户,以此结束本章以及本书。将要演示的是使用Google账号认证,但ASP.NET Identity对于Microsoft、Facebook以及Twitter账号,都有内建的支持。表15-1是本章概要。 Table 15-1. Chapter Summary 表15-1. 本章概要 Problem 问题 Solution 解决方案 Listing 清单号 Store additional information about users. 存储用户的附加信息 Define custom user properties. 定义自定义用户属性 1–3, 8–11 Update the database schema without deleting user data. 更新数据库架构而不删除用户数据 Perform a database migration. 执行数据库迁移 4–7 Perform fine-grained authorization. 执行细粒度授权 Use claims. 使用声明(Claims) 12–14 Add claims about a user. 添加用户的声明(Claims) Use the ClaimsIdentity.AddClaims method. 使用ClaimsIdentity.AddClaims方法 15–19 Authorize access based on claim values. 基于声明(Claims)值授权访问 Create a custom authorization filter attribute. 创建一个自定义的授权过滤器注解属性 20–21 Authenticate through a third party. 通过第三方认证 Install the NuGet package for the authentication provider, redirect requests to that provider, and specify a callback URL that creates the user account. 安装认证提供器的NuGet包,将请求重定向到该提供器,并指定一个创建用户账号的回调URL。 22–25 15.1 Preparing the Example Project 15.1 准备示例项目 In this chapter, I am going to continue working on the Users project I created in Chapter 13 and enhanced in Chapter 14. No changes to the application are required, but start the application and make sure that there are users in the database. Figure 15-1 shows the state of my database, which contains the users Admin, Alice, Bob, and Joe from the previous chapter. To check the users, start the application and request the /Admin/Index URL and authenticate as the Admin user. 本章打算继续使用第13章创建并在第14章增强的Users项目。对应用程序无需做什么改变,但需要启动应用程序,并确保数据库中有一些用户。图15-1显示了数据库的状态,它含有上一章的用户Admin、Alice、Bob以及Joe。为了检查用户,请启动应用程序,请求/Admin/Index URL,并以Admin用户进行认证。 Figure 15-1. The initial users in the Identity database 图15-1. Identity数据库中的最初用户 I also need some roles for this chapter. I used the RoleAdmin controller to create roles called Users and Employees and assigned the users to those roles, as described in Table 15-2. 本章还需要一些角色。我用RoleAdmin控制器创建了角色Users和Employees,并为这些角色指定了一些用户,如表15-2所示。 Table 15-2. The Types of Web Forms Code Nuggets 表15-2. 角色及成员(作者将此表的标题写错了——译者注) Role 角色 Members 成员 Users Alice, Joe Employees Alice, Bob Figure 15-2 shows the required role configuration displayed by the RoleAdmin controller. 图15-2显示了由RoleAdmin控制器所显示出来的必要的角色配置。 Figure 15-2. Configuring the roles required for this chapter 图15-2. 配置本章所需的角色 15.2 Adding Custom User Properties 15.2 添加自定义用户属性 When I created the AppUser class to represent users in Chapter 13, I noted that the base class defined a basic set of properties to describe the user, such as e-mail address and telephone number. Most applications need to store more information about users, including persistent application preferences and details such as addresses—in short, any data that is useful to running the application and that should last between sessions. In ASP.NET Membership, this was handled through the user profile system, but ASP.NET Identity takes a different approach. 我在第13章创建AppUser类来表示用户时曾做过说明,基类定义了一组描述用户的基本属性,如E-mail地址、电话号码等。大多数应用程序还需要存储用户的更多信息,包括持久化应用程序爱好以及地址等细节——简言之,需要存储对运行应用程序有用并且在各次会话之间应当保持的任何数据。在ASP.NET Membership中,这是通过用户资料(User Profile)系统来处理的,但ASP.NET Identity采取了一种不同的办法。 Because the ASP.NET Identity system uses Entity Framework to store its data by default, defining additional user information is just a matter of adding properties to the user class and letting the Code First feature create the database schema required to store them. Table 15-3 puts custom user properties in context. 因为ASP.NET Identity默认是使用Entity Framework来存储其数据的,定义附加的用户信息只不过是给用户类添加属性的事情,然后让Code First特性去创建需要存储它们的数据库架构即可。表15-3描述了自定义用户属性的情形。 Table 15-3. Putting Cusotm User Properties in Context 表15-3. 自定义用户属性的情形 Question 问题 Answer 回答 What is it? 什么是自定义用户属性? Custom user properties allow you to store additional information about your users, including their preferences and settings. 自定义用户属性让你能够存储附加的用户信息,包括他们的爱好和设置。 Why should I care? 为何要关心它? A persistent store of settings means that the user doesn’t have to provide the same information each time they log in to the application. 设置的持久化存储意味着,用户不必每次登录到应用程序时都提供同样的信息。 How is it used by the MVC framework? 在MVC框架中如何使用它? This feature isn’t used directly by the MVC framework, but it is available for use in action methods. 此特性不是由MVC框架直接使用的,但它在动作方法中使用是有效的。 15.2.1 Defining Custom Properties 15.2.1 定义自定义属性 Listing 15-1 shows how I added a simple property to the AppUser class to represent the city in which the user lives. 清单15-1演示了如何给AppUser类添加一个简单的属性,用以表示用户生活的城市。 Listing 15-1. Adding a Property in the AppUser.cs File 清单15-1. 在AppUser.cs文件中添加属性 using System;using Microsoft.AspNet.Identity.EntityFramework;namespace Users.Models { public enum Cities {LONDON, PARIS, CHICAGO}public class AppUser : IdentityUser {public Cities City { get; set; } }} I have defined an enumeration called Cities that defines values for some large cities and added a property called City to the AppUser class. To allow the user to view and edit their City property, I added actions to the Home controller, as shown in Listing 15-2. 这里定义了一个枚举,名称为Cities,它定义了一些大城市的值,另外给AppUser类添加了一个名称为City的属性。为了让用户能够查看和编辑City属性,给Home控制器添加了几个动作方法,如清单15-2所示。 Listing 15-2. Adding Support for Custom User Properties in the HomeController.cs File 清单15-2. 在HomeController.cs文件中添加对自定义属性的支持 using System.Web.Mvc;using System.Collections.Generic;using System.Web;using System.Security.Principal;using System.Threading.Tasks;using Users.Infrastructure;using Microsoft.AspNet.Identity;using Microsoft.AspNet.Identity.Owin;using Users.Models;namespace Users.Controllers {public class HomeController : Controller {[Authorize]public ActionResult Index() {return View(GetData("Index"));}[Authorize(Roles = "Users")]public ActionResult OtherAction() {return View("Index", GetData("OtherAction"));}private Dictionary<string, object> GetData(string actionName) {Dictionary<string, object> dict= new Dictionary<string, object>();dict.Add("Action", actionName);dict.Add("User", HttpContext.User.Identity.Name);dict.Add("Authenticated", HttpContext.User.Identity.IsAuthenticated);dict.Add("Auth Type", HttpContext.User.Identity.AuthenticationType);dict.Add("In Users Role", HttpContext.User.IsInRole("Users"));return dict;} [Authorize]public ActionResult UserProps() {return View(CurrentUser);}[Authorize][HttpPost]public async Task<ActionResult> UserProps(Cities city) {AppUser user = CurrentUser;user.City = city;await UserManager.UpdateAsync(user);return View(user);}private AppUser CurrentUser {get {return UserManager.FindByName(HttpContext.User.Identity.Name);} }private AppUserManager UserManager {get {return HttpContext.GetOwinContext().GetUserManager<AppUserManager>();} }} } I added a CurrentUser property that uses the AppUserManager class to retrieve an AppUser instance to represent the current user. I pass the AppUser object as the view model object in the GET version of the UserProps action method, and the POST method uses it to update the value of the new City property. Listing 15-3 shows the UserProps.cshtml view, which displays the City property value and contains a form to change it. 我添加了一个CurrentUser属性,它使用AppUserManager类接收了表示当前用户的AppUser实例。在GET版本的UserProps动作方法中,传递了这个AppUser对象作为视图模型。而在POST版的方法中用它更新了City属性的值。清单15-3显示了UserProps.cshtml视图,它显示了City属性的值,并包含一个修改它的表单。 Listing 15-3. The Contents of the UserProps.cshtml File in the Views/Home Folder 清单15-3. Views/Home文件夹中UserProps.cshtml文件的内容 @using Users.Models@model AppUser@{ ViewBag.Title = "UserProps";}<div class="panel panel-primary"><div class="panel-heading">Custom User Properties</div><table class="table table-striped"><tr><th>City</th><td>@Model.City</td></tr></table></div> @using (Html.BeginForm()) {<div class="form-group"><label>City</label>@Html.DropDownListFor(x => x.City, new SelectList(Enum.GetNames(typeof(Cities))))</div><button class="btn btn-primary" type="submit">Save</button>} Caution Don’t start the application when you have created the view. In the sections that follow, I demonstrate how to preserve the contents of the database, and if you start the application now, the ASP.NET Identity users will be deleted. 警告:创建了视图之后不要启动应用程序。在以下小节中,将演示如何保留数据库的内容,如果现在启动应用程序,将会删除ASP.NET Identity的用户。 15.2.2 Preparing for Database Migration 15.2.2 准备数据库迁移 The default behavior for the Entity Framework Code First feature is to drop the tables in the database and re-create them whenever classes that drive the schema have changed. You saw this in Chapter 14 when I added support for roles: When the application was started, the database was reset, and the user accounts were lost. Entity Framework Code First特性的默认行为是,一旦修改了派生数据库架构的类,便会删除数据库中的数据表,并重新创建它们。在第14章可以看到这种情况,在我添加角色支持时:当重启应用程序后,数据库被重置,用户账号也丢失。 Don’t start the application yet, but if you were to do so, you would see a similar effect. Deleting data during development is usually not a problem, but doing so in a production setting is usually disastrous because it deletes all of the real user accounts and causes a panic while the backups are restored. In this section, I am going to demonstrate how to use the database migration feature, which updates a Code First schema in a less brutal manner and preserves the existing data it contains. 不要启动应用程序,但如果你这么做了,会看到类似的效果。在开发期间删除数据没什么问题,但如果在产品设置中这么做了,通常是灾难性的,因为它会删除所有真实的用户账号,而备份恢复是很痛苦的事。在本小节中,我打算演示如何使用数据库迁移特性,它能以比较温和的方式更新Code First的架构,并保留架构中的已有数据。 The first step is to issue the following command in the Visual Studio Package Manager Console: 第一个步骤是在Visual Studio的“Package Manager Console(包管理器控制台)”中发布以下命令: Enable-Migrations –EnableAutomaticMigrations This enables the database migration support and creates a Migrations folder in the Solution Explorer that contains a Configuration.cs class file, the contents of which are shown in Listing 15-4. 它启用了数据库的迁移支持,并在“Solution Explorer(解决方案资源管理器)”创建一个Migrations文件夹,其中含有一个Configuration.cs类文件,内容如清单15-4所示。 Listing 15-4. The Contents of the Configuration.cs File 清单15-4. Configuration.cs文件的内容 namespace Users.Migrations {using System;using System.Data.Entity;using System.Data.Entity.Migrations;using System.Linq;internal sealed class Configuration: DbMigrationsConfiguration<Users.Infrastructure.AppIdentityDbContext> {public Configuration() {AutomaticMigrationsEnabled = true;ContextKey = "Users.Infrastructure.AppIdentityDbContext";}protected override void Seed(Users.Infrastructure.AppIdentityDbContext context) {// This method will be called after migrating to the latest version.// 此方法将在迁移到最新版本时调用// You can use the DbSet<T>.AddOrUpdate() helper extension method// to avoid creating duplicate seed data. E.g.// 例如,你可以使用DbSet<T>.AddOrUpdate()辅助器方法来避免创建重复的种子数据//// context.People.AddOrUpdate(// p => p.FullName,// new Person { FullName = "Andrew Peters" },// new Person { FullName = "Brice Lambson" },// new Person { FullName = "Rowan Miller" }// );//} }} Tip You might be wondering why you are entering a database migration command into the console used to manage NuGet packages. The answer is that the Package Manager Console is really PowerShell, which is a general-purpose tool that is mislabeled by Visual Studio. You can use the console to issue a wide range of helpful commands. See http://go.microsoft.com/fwlink/?LinkID=108518 for details. 提示:你可能会觉得奇怪,为什么要在管理NuGet包的控制台中输入数据库迁移的命令?答案是“Package Manager Console(包管理控制台)”是真正的PowerShell,这是Visual studio冒用的一个通用工具。你可以使用此控制台发送大量的有用命令,详见http://go.microsoft.com/fwlink/?LinkID=108518。 The class will be used to migrate existing content in the database to the new schema, and the Seed method will be called to provide an opportunity to update the existing database records. In Listing 15-5, you can see how I have used the Seed method to set a default value for the new City property I added to the AppUser class. (I have also updated the class file to reflect my usual coding style.) 这个类将用于把数据库中的现有内容迁移到新的数据库架构,Seed方法的调用为更新现有数据库记录提供了机会。在清单15-5中可以看到,我如何用Seed方法为新的City属性设置默认值,City是添加到AppUser类中自定义属性。(为了体现我一贯的编码风格,我对这个类文件也进行了更新。) Listing 15-5. Managing Existing Content in the Configuration.cs File 清单15-5. 在Configuration.cs文件中管理已有内容 using System.Data.Entity.Migrations;using Microsoft.AspNet.Identity;using Microsoft.AspNet.Identity.EntityFramework;using Users.Infrastructure;using Users.Models;namespace Users.Migrations {internal sealed class Configuration: DbMigrationsConfiguration<AppIdentityDbContext> {public Configuration() {AutomaticMigrationsEnabled = true;ContextKey = "Users.Infrastructure.AppIdentityDbContext";}protected override void Seed(AppIdentityDbContext context) {AppUserManager userMgr = new AppUserManager(new UserStore<AppUser>(context));AppRoleManager roleMgr = new AppRoleManager(new RoleStore<AppRole>(context)); string roleName = "Administrators";string userName = "Admin";string password = "MySecret";string email = "admin@example.com";if (!roleMgr.RoleExists(roleName)) {roleMgr.Create(new AppRole(roleName));}AppUser user = userMgr.FindByName(userName);if (user == null) {userMgr.Create(new AppUser { UserName = userName, Email = email },password);user = userMgr.FindByName(userName);}if (!userMgr.IsInRole(user.Id, roleName)) {userMgr.AddToRole(user.Id, roleName);}foreach (AppUser dbUser in userMgr.Users) {dbUser.City = Cities.PARIS;}context.SaveChanges();} }} You will notice that much of the code that I added to the Seed method is taken from the IdentityDbInit class, which I used to seed the database with an administration user in Chapter 14. This is because the new Configuration class added to support database migrations will replace the seeding function of the IdentityDbInit class, which I’ll update shortly. Aside from ensuring that there is an admin user, the statements in the Seed method that are important are the ones that set the initial value for the City property I added to the AppUser class, as follows: 你可能会注意到,添加到Seed方法中的许多代码取自于IdentityDbInit类,在第14章中我用这个类将管理用户植入了数据库。这是因为这个新添加的、用以支持数据库迁移的Configuration类,将代替IdentityDbInit类的种植功能,我很快便会更新这个类。除了要确保有admin用户之外,在Seed方法中的重要语句是那些为AppUser类的City属性设置初值的语句,如下所示: ...foreach (AppUser dbUser in userMgr.Users) { dbUser.City = Cities.PARIS;}context.SaveChanges();... You don’t have to set a default value for new properties—I just wanted to demonstrate that the Seed method in the Configuration class can be used to update the existing user records in the database. 你不一定要为新属性设置默认值——这里只是想演示Configuration类中的Seed方法,可以用它更新数据库中的已有用户记录。 Caution Be careful when setting values for properties in the Seed method for real projects because the values will be applied every time you change the schema, overriding any values that the user has set since the last schema update was performed. I set the value of the City property just to demonstrate that it can be done. 警告:在用于真实项目的Seed方法中为属性设置值时要小心,因为你每一次修改架构时,都会运用这些值,这会将自执行上一次架构更新之后,用户设置的任何数据覆盖掉。这里设置City属性的值只是为了演示它能够这么做。 Changing the Database Context Class 修改数据库上下文类 The reason that I added the seeding code to the Configuration class is that I need to change the IdentityDbInit class. At present, the IdentityDbInit class is derived from the descriptively named DropCreateDatabaseIfModelChanges<AppIdentityDbContext> class, which, as you might imagine, drops the entire database when the Code First classes change. Listing 15-6 shows the changes I made to the IdentityDbInit class to prevent it from affecting the database. 在Configuration类中添加种植代码的原因是我需要修改IdentityDbInit类。此时,IdentityDbInit类派生于描述性命名的DropCreateDatabaseIfModelChanges<AppIdentityDbContext> 类,和你相像的一样,它会在Code First类改变时删除整个数据库。清单15-6显示了我对IdentityDbInit类所做的修改,以防止它影响数据库。 Listing 15-6. Preventing Database Schema Changes in the AppIdentityDbContext.cs File 清单15-6. 在AppIdentityDbContext.cs文件是阻止数据库架构变化 using System.Data.Entity;using Microsoft.AspNet.Identity.EntityFramework;using Users.Models;using Microsoft.AspNet.Identity; namespace Users.Infrastructure {public class AppIdentityDbContext : IdentityDbContext<AppUser> {public AppIdentityDbContext() : base("IdentityDb") { }static AppIdentityDbContext() {Database.SetInitializer<AppIdentityDbContext>(new IdentityDbInit());}public static AppIdentityDbContext Create() {return new AppIdentityDbContext();} } public class IdentityDbInit : NullDatabaseInitializer<AppIdentityDbContext> {} } I have removed the methods defined by the class and changed its base to NullDatabaseInitializer<AppIdentityDbContext> , which prevents the schema from being altered. 我删除了这个类中所定义的方法,并将它的基类改为NullDatabaseInitializer<AppIdentityDbContext> ,它可以防止架构修改。 15.2.3 Performing the Migration 15.2.3 执行迁移 All that remains is to generate and apply the migration. First, run the following command in the Package Manager Console: 剩下的事情只是生成并运用迁移了。首先,在“Package Manager Console(包管理器控制台)”中执行以下命令: Add-Migration CityProperty This creates a new migration called CityProperty (I like my migration names to reflect the changes I made). A class new file will be added to the Migrations folder, and its name reflects the time at which the command was run and the name of the migration. My file is called 201402262244036_CityProperty.cs, for example. The contents of this file contain the details of how Entity Framework will change the database during the migration, as shown in Listing 15-7. 这创建了一个名称为CityProperty的新迁移(我比较喜欢让迁移的名称反映出我所做的修改)。这会在文件夹中添加一个新的类文件,而且其命名会反映出该命令执行的时间以及迁移名称,例如,我的这个文件名称为201402262244036_CityProperty.cs。该文件的内容含有迁移期间Entity Framework修改数据库的细节,如清单15-7所示。 Listing 15-7. The Contents of the 201402262244036_CityProperty.cs File 清单15-7. 201402262244036_CityProperty.cs文件的内容 namespace Users.Migrations {using System;using System.Data.Entity.Migrations; public partial class Init : DbMigration {public override void Up() {AddColumn("dbo.AspNetUsers", "City", c => c.Int(nullable: false));}public override void Down() {DropColumn("dbo.AspNetUsers", "City");} }} The Up method describes the changes that have to be made to the schema when the database is upgraded, which in this case means adding a City column to the AspNetUsers table, which is the one that is used to store user records in the ASP.NET Identity database. Up方法描述了在数据库升级时,需要对架构所做的修改,在这个例子中,意味着要在AspNetUsers数据表中添加City数据列,该数据表是ASP.NET Identity数据库用来存储用户记录的。 The final step is to perform the migration. Without starting the application, run the following command in the Package Manager Console: 最后一步是执行迁移。无需启动应用程序,只需在“Package Manager Console(包管理器控制台)”中运行以下命令即可: Update-Database –TargetMigration CityProperty The database schema will be modified, and the code in the Configuration.Seed method will be executed. The existing user accounts will have been preserved and enhanced with a City property (which I set to Paris in the Seed method). 这会修改数据库架构,并执行Configuration.Seed方法中的代码。已有用户账号会被保留,且增强了City属性(我在Seed方法中已将其设置为“Paris”)。 15.2.4 Testing the Migration 15.2.4 测试迁移 To test the effect of the migration, start the application, navigate to the /Home/UserProps URL, and authenticate as one of the Identity users (for example, as Alice with the password MySecret). Once authenticated, you will see the current value of the City property for the user and have the opportunity to change it, as shown in Figure 15-3. 为了测试迁移的效果,启动应用程序,导航到/Home/UserProps URL,并以Identity中的用户(例如Alice,口令MySecret)进行认证。一旦已被认证,便会看到该用户City属性的当前值,并可以对其进行修改,如图15-3所示。 Figure 15-3. Displaying and changing a custom user property 图15-3. 显示和个性自定义用户属性 15.2.5 Defining an Additional Property 15.2.5 定义附加属性 Now that database migrations are set up, I am going to define a further property just to demonstrate how subsequent changes are handled and to show a more useful (and less dangerous) example of using the Configuration.Seed method. Listing 15-8 shows how I added a Country property to the AppUser class. 现在,已经建立了数据库迁移,我打算再定义一个属性,这恰恰演示了如何处理持续不断的修改,也为了演示Configuration.Seed方法更有用(至少无害)的示例。清单15-8显示了我在AppUser类上添加了一个Country属性。 Listing 15-8. Adding Another Property in the AppUserModels.cs File 清单15-8. 在AppUserModels.cs文件中添加另一个属性 using System;using Microsoft.AspNet.Identity.EntityFramework; namespace Users.Models {public enum Cities {LONDON, PARIS, CHICAGO} public enum Countries {NONE, UK, FRANCE, USA}public class AppUser : IdentityUser {public Cities City { get; set; }public Countries Country { get; set; }public void SetCountryFromCity(Cities city) {switch (city) {case Cities.LONDON:Country = Countries.UK;break;case Cities.PARIS:Country = Countries.FRANCE;break;case Cities.CHICAGO:Country = Countries.USA;break;default:Country = Countries.NONE;break;} }} } I have added an enumeration to define the country names and a helper method that selects a country value based on the City property. Listing 15-9 shows the change I made to the Configuration class so that the Seed method sets the Country property based on the City, but only if the value of Country is NONE (which it will be for all users when the database is migrated because the Entity Framework sets enumeration columns to the first value). 我已经添加了一个枚举,它定义了国家名称。还添加了一个辅助器方法,它可以根据City属性选择一个国家。清单15-9显示了对Configuration类所做的修改,以使Seed方法根据City设置Country属性,但只当Country为NONE时才进行设置(在迁移数据库时,所有用户都是NONE,因为Entity Framework会将枚举列设置为枚举的第一个值)。 Listing 15-9. Modifying the Database Seed in the Configuration.cs File 清单15-9. 在Configuration.cs文件中修改数据库种子 using System.Data.Entity.Migrations;using Microsoft.AspNet.Identity;using Microsoft.AspNet.Identity.EntityFramework;using Users.Infrastructure;using Users.Models; namespace Users.Migrations {internal sealed class Configuration: DbMigrationsConfiguration<AppIdentityDbContext> {public Configuration() {AutomaticMigrationsEnabled = true;ContextKey = "Users.Infrastructure.AppIdentityDbContext";}protected override void Seed(AppIdentityDbContext context) {AppUserManager userMgr = new AppUserManager(new UserStore<AppUser>(context));AppRoleManager roleMgr = new AppRoleManager(new RoleStore<AppRole>(context)); string roleName = "Administrators";string userName = "Admin";string password = "MySecret";string email = "admin@example.com";if (!roleMgr.RoleExists(roleName)) {roleMgr.Create(new AppRole(roleName));}AppUser user = userMgr.FindByName(userName);if (user == null) {userMgr.Create(new AppUser { UserName = userName, Email = email },password);user = userMgr.FindByName(userName);}if (!userMgr.IsInRole(user.Id, roleName)) {userMgr.AddToRole(user.Id, roleName);} foreach (AppUser dbUser in userMgr.Users) {if (dbUser.Country == Countries.NONE) {dbUser.SetCountryFromCity(dbUser.City);} }context.SaveChanges();} }} This kind of seeding is more useful in a real project because it will set a value for the Country property only if one has not already been set—subsequent migrations won’t be affected, and user selections won’t be lost. 这种种植在实际项目中会更有用,因为它只会在Country属性未设置时,才会设置Country属性的值——后继的迁移不会受到影响,因此不会失去用户的选择。 1. Adding Application Support 1. 添加应用程序支持 There is no point defining additional user properties if they are not available in the application, so Listing 15-10 shows the change I made to the Views/Home/UserProps.cshtml file to display the value of the Country property. 应用程序中如果没有定义附加属性的地方,则附加属性就无法使用了,因此,清单15-10显示了我对Views/Home/UserProps.cshtml文件的修改,以显示Country属性的值。 Listing 15-10. Displaying an Additional Property in the UserProps.cshtml File 清单15-10. 在UserProps.cshtml文件中显示附加属性 @using Users.Models@model AppUser@{ ViewBag.Title = "UserProps";} <div class="panel panel-primary"><div class="panel-heading">Custom User Properties</div><table class="table table-striped"><tr><th>City</th><td>@Model.City</td></tr> <tr><th>Country</th><td>@Model.Country</td></tr></table></div>@using (Html.BeginForm()) {<div class="form-group"><label>City</label>@Html.DropDownListFor(x => x.City, new SelectList(Enum.GetNames(typeof(Cities))))</div><button class="btn btn-primary" type="submit">Save</button>} Listing 15-11 shows the corresponding change I made to the Home controller to update the Country property when the City value changes. 为了在City值变化时能够更新Country属性,清单15-11显示了我对Home控制器所做的相应修改。 Listing 15-11. Setting Custom Properties in the HomeController.cs File 清单15-11. 在HomeController.cs文件中设置自定义属性 using System.Web.Mvc;using System.Collections.Generic;using System.Web;using System.Security.Principal;using System.Threading.Tasks;using Users.Infrastructure;using Microsoft.AspNet.Identity;using Microsoft.AspNet.Identity.Owin;using Users.Models; namespace Users.Controllers {public class HomeController : Controller {// ...other action methods omitted for brevity...// ...出于简化,这里忽略了其他动作方法... [Authorize]public ActionResult UserProps() {return View(CurrentUser);}[Authorize][HttpPost]public async Task<ActionResult> UserProps(Cities city) {AppUser user = CurrentUser;user.City = city;user.SetCountryFromCity(city);await UserManager.UpdateAsync(user);return View(user);}// ...properties omitted for brevity...// ...出于简化,这里忽略了一些属性...} } 2. Performing the Migration 2. 准备迁移 All that remains is to create and apply a new migration. Enter the following command into the Package Manager Console: 剩下的事情就是创建和运用新的迁移了。在“Package Manager Console(包管理器控制台)”中输入以下命令: Add-Migration CountryProperty This will generate another file in the Migrations folder that contains the instruction to add the Country column. To apply the migration, execute the following command: 这将在Migrations文件夹中生成另一个文件,它含有添加Country数据表列的指令。为了运用迁移,可执行以下命令: Update-Database –TargetMigration CountryProperty The migration will be performed, and the value of the Country property will be set based on the value of the existing City property for each user. You can check the new user property by starting the application and authenticating and navigating to the /Home/UserProps URL, as shown in Figure 15-4. 这将执行迁移,Country属性的值将根据每个用户当前的City属性进行设置。通过启动应用程序,认证并导航到/Home/UserProps URL,便可以查看新的用户属性,如图15-4所示。 Figure 15-4. Creating an additional user property 图15-4. 创建附加用户属性 Tip Although I am focused on the process of upgrading the database, you can also migrate back to a previous version by specifying an earlier migration. Use the –Force argument make changes that cause data loss, such as removing a column. 提示:虽然我们关注了升级数据库的过程,但你也可以回退到以前的版本,只需指定一个早期的迁移即可。使用-Force参数进行修改,会引起数据丢失,例如删除数据表列。 15.3 Working with Claims 15.3 使用声明(Claims) In older user-management systems, such as ASP.NET Membership, the application was assumed to be the authoritative source of all information about the user, essentially treating the application as a closed world and trusting the data that is contained within it. 在旧的用户管理系统中,例如ASP.NET Membership,应用程序被假设成是用户所有信息的权威来源,本质上将应用程序视为是一个封闭的世界,并且只信任其中所包含的数据。 This is such an ingrained approach to software development that it can be hard to recognize that’s what is happening, but you saw an example of the closed-world technique in Chapter 14 when I authenticated users against the credentials stored in the database and granted access based on the roles associated with those credentials. I did the same thing again in this chapter when I added properties to the user class. Every piece of information that I needed to manage user authentication and authorization came from within my application—and that is a perfectly satisfactory approach for many web applications, which is why I demonstrated these techniques in such depth. 这是软件开发的一种根深蒂固的方法,使人很难认识到这到底意味着什么,第14章你已看到了这种封闭世界技术的例子,根据存储在数据库中的凭据来认证用户,并根据与凭据关联在一起的角色来授权访问。本章前述在用户类上添加属性,也做了同样的事情。我管理用户认证与授权所需的每一个数据片段都来自于我的应用程序——而且这是许多Web应用程序都相当满意的一种方法,这也是我如此深入地演示这些技术的原因。 ASP.NET Identity also supports an alternative approach for dealing with users, which works well when the MVC framework application isn’t the sole source of information about users and which can be used to authorize users in more flexible and fluid ways than traditional roles allow. ASP.NET Identity还支持另一种处理用户的办法,当MVC框架的应用程序不是有关用户的唯一信息源时,这种办法会工作得很好,而且能够比传统的角色授权更为灵活且流畅的方式进行授权。 This alternative approach uses claims, and in this section I’ll describe how ASP.NET Identity supports claims-based authorization. Table 15-4 puts claims in context. 这种可选的办法使用了“Claims(声明)”,因此在本小节中,我将描述ASP.NET Identity如何支持“Claims-Based Authorization(基于声明的授权)”。表15-4描述了声明(Claims)的情形。 提示:“Claim”在英文字典中不完全是“声明”的意思,根据本文的描述,感觉把它说成“声明”也不一定合适,所以在之后的译文中基本都写成中英文并用的形式,即“声明(Claims)”。根据表15-4中的声明(Claims)的定义:声明(Claims)是关于用户的一些信息片段。一个用户的信息片段当然有很多,每一个信息片段就是一项声明(Claim),用户的所有信息片段合起来就是该用户的声明(Claims)。请读者注意该单词的单复数形式——译者注 Table 15-4. Putting Claims in Context 表15-4. 声明(Claims)的情形 Question 问题 Answer 答案 What is it? 什么是声明(Claims)? Claims are pieces of information about users that you can use to make authorization decisions. Claims can be obtained from external systems as well as from the local Identity database. 声明(Claims)是关于用户的一些信息片段,可以用它们做出授权决定。声明(Claims)可以从外部系统获取,也可以从本地的Identity数据库获取。 Why should I care? 为何要关心它? Claims can be used to flexibly authorize access to action methods. Unlike conventional roles, claims allow access to be driven by the information that describes the user. 声明(Claims)可以用来对动作方法进行灵活的授权访问。与传统的角色不同,声明(Claims)让访问能够由描述用户的信息进行驱动。 How is it used by the MVC framework? 如何在MVC框架中使用它? This feature isn’t used directly by the MVC framework, but it is integrated into the standard authorization features, such as the Authorize attribute. 这不是直接由MVC框架使用的特性,但它集成到了标准的授权特性之中,例如Authorize注解属性。 Tip you don’t have to use claims in your applications, and as Chapter 14 showed, ASP.NET Identity is perfectly happy providing an application with the authentication and authorization services without any need to understand claims at all. 提示:你在应用程序中不一定要使用声明(Claims),正如第14章所展示的那样,ASP.NET Identity能够为应用程序提供充分的认证与授权服务,而根本不需要理解声明(Claims)。 15.3.1 Understanding Claims 15.3.1 理解声明(Claims) A claim is a piece of information about the user, along with some information about where the information came from. The easiest way to unpack claims is through some practical demonstrations, without which any discussion becomes too abstract to be truly useful. To get started, I added a Claims controller to the example project, the definition of which you can see in Listing 15-12. 一项声明(Claim)是关于用户的一个信息片段(请注意这个英文单词的单复数形式——译者注),并伴有该片段出自何处的某种信息。揭开声明(Claims)含义最容易的方式是做一些实际演示,任何讨论都会过于抽象根本没有真正的用处。为此,我在示例项目中添加了一个Claims控制器,其定义如清单15-12所示。 Listing 15-12. The Contents of the ClaimsController.cs File 清单15-12. ClaimsController.cs文件的内容 using System.Security.Claims;using System.Web;using System.Web.Mvc; namespace Users.Controllers {public class ClaimsController : Controller {[Authorize]public ActionResult Index() {ClaimsIdentity ident = HttpContext.User.Identity as ClaimsIdentity;if (ident == null) {return View("Error", new string[] { "No claims available" });} else {return View(ident.Claims);} }} } Tip You may feel a little lost as I define the code for this example. Don’t worry about the details for the moment—just stick with it until you see the output from the action method and view that I define. More than anything else, that will help put claims into perspective. 提示:你或许会对我为此例定义的代码感到有点失望。此刻对此细节不必着急——只要稍事忍耐,当看到该动作方法和视图的输出便会明白。尤为重要的是,这有助于洞察声明(Claims)。 You can get the claims associated with a user in different ways. One approach is to use the Claims property defined by the user class, but in this example, I have used the HttpContext.User.Identity property to demonstrate the way that ASP.NET Identity is integrated with the rest of the ASP.NET platform. As I explained in Chapter 13, the HttpContext.User.Identity property returns an implementation of the IIdentity interface, which is a ClaimsIdentity object when working using ASP.NET Identity. The ClaimsIdentity class is defined in the System.Security.Claims namespace, and Table 15-5 shows the members it defines that are relevant to this chapter. 可以通过不同的方式获得与用户相关联的声明(Claims)。方法之一就是使用由用户类定义的Claims属性,但在这个例子中,我使用了HttpContext.User.Identity属性,目的是演示ASP.NET Identity与ASP.NET平台集成的方式(请注意这句话所表示的含义:用户类的Claims属性属于ASP.NET Identity,而HttpContext.User.Identity属性则属于ASP.NET平台。由此可见,ASP.NET Identity已经融合到了ASP.NET平台之中——译者注)。正如第13章所解释的那样,HttpContext.User.Identity属性返回IIdentity的接口实现,当使用ASP.NET Identity时,该实现是一个ClaimsIdentity对象。ClaimsIdentity类是在System.Security.Claims命名空间中定义的,表15-5显示了它所定义的与本章有关的成员。 Table 15-5. The Members Defined by the ClaimsIdentity Class 表15-5. ClaimsIdentity类所定义的成员 Name 名称 Description 描述 Claims Returns an enumeration of Claim objects representing the claims for the user. 返回表示用户声明(Claims)的Claim对象枚举 AddClaim(claim) Adds a claim to the user identity. 给用户添加一个声明(Claim) AddClaims(claims) Adds an enumeration of Claim objects to the user identity. 给用户添加Claim对象的枚举。 HasClaim(predicate) Returns true if the user identity contains a claim that matches the specified predicate. See the “Applying Claims” section for an example predicate. 如果用户含有与指定谓词匹配的声明(Claim)时,返回true。参见“运用声明(Claims)”中的示例谓词 RemoveClaim(claim) Removes a claim from the user identity. 删除用户的声明(Claim)。 Other members are available, but the ones in the table are those that are used most often in web applications, for reason that will become obvious as I demonstrate how claims fit into the wider ASP.NET platform. 还有一些可用的其它成员,但表中的这些是在Web应用程序中最常用的,随着我演示如何将声明(Claims)融入更宽泛的ASP.NET平台,它们为什么最常用就很显然了。 In Listing 15-12, I cast the IIdentity implementation to the ClaimsIdentity type and pass the enumeration of Claim objects returned by the ClaimsIdentity.Claims property to the View method. A Claim object represents a single piece of data about the user, and the Claim class defines the properties shown in Table 15-6. 在清单15-12中,我将IIdentity实现转换成了ClaimsIdentity类型,并且给View方法传递了ClaimsIdentity.Claims属性所返回的Claim对象的枚举。Claim对象所示表示的是关于用户的一个单一的数据片段,Claim类定义的属性如表15-6所示。 Table 15-6. The Properties Defined by the Claim Class 表15-6. Claim类定义的属性 Name 名称 Description 描述 Issuer Returns the name of the system that provided the claim 返回提供声明(Claim)的系统名称 Subject Returns the ClaimsIdentity object for the user who the claim refers to 返回声明(Claim)所指用户的ClaimsIdentity对象 Type Returns the type of information that the claim represents 返回声明(Claim)所表示的信息类型 Value Returns the piece of information that the claim represents 返回声明(Claim)所表示的信息片段 Listing 15-13 shows the contents of the Index.cshtml file that I created in the Views/Claims folder and that is rendered by the Index action of the Claims controller. The view adds a row to a table for each claim about the user. 清单15-13显示了我在Views/Claims文件夹中创建的Index.cshtml文件的内容,它由Claims控制器中的Index动作方法进行渲染。该视图为用户的每项声明(Claim)添加了一个表格行。 Listing 15-13. The Contents of the Index.cshtml File in the Views/Claims Folder 清单15-13. Views/Claims文件夹中Index.cshtml文件的内容 @using System.Security.Claims@using Users.Infrastructure@model IEnumerable<Claim>@{ ViewBag.Title = "Claims"; }<div class="panel panel-primary"><div class="panel-heading">Claims</div><table class="table table-striped"><tr><th>Subject</th><th>Issuer</th><th>Type</th><th>Value</th></tr>@foreach (Claim claim in Model.OrderBy(x => x.Type)) {<tr><td>@claim.Subject.Name</td><td>@claim.Issuer</td><td>@Html.ClaimType(claim.Type)</td><td>@claim.Value</td></tr>}</table></div> The value of the Claim.Type property is a URI for a Microsoft schema, which isn’t especially useful. The popular schemas are used as the values for fields in the System.Security.Claims.ClaimTypes class, so to make the output from the Index.cshtml view easier to read, I added an HTML helper to the IdentityHelpers.cs file, as shown in Listing 15-14. It is this helper that I use in the Index.cshtml file to format the value of the Claim.Type property. Claim.Type属性的值是一个微软模式(Microsoft Schema)的URI(统一资源标识符),这是特别有用的。System.Security.Claims.ClaimTypes类中字段的值使用的是流行模式(Popular Schema),因此为了使Index.cshtml视图的输出更易于阅读,我在IdentityHelpers.cs文件中添加了一个HTML辅助器,如清单15-14所示。Index.cshtml文件正是使用这个辅助器格式化了Claim.Type属性的值。 Listing 15-14. Adding a Helper to the IdentityHelpers.cs File 清单15-14. 在IdentityHelpers.cs文件中添加辅助器 using System.Web;using System.Web.Mvc;using Microsoft.AspNet.Identity.Owin;using System;using System.Linq;using System.Reflection;using System.Security.Claims;namespace Users.Infrastructure {public static class IdentityHelpers {public static MvcHtmlString GetUserName(this HtmlHelper html, string id) {AppUserManager mgr= HttpContext.Current.GetOwinContext().GetUserManager<AppUserManager>();return new MvcHtmlString(mgr.FindByIdAsync(id).Result.UserName);} public static MvcHtmlString ClaimType(this HtmlHelper html, string claimType) {FieldInfo[] fields = typeof(ClaimTypes).GetFields();foreach (FieldInfo field in fields) {if (field.GetValue(null).ToString() == claimType) {return new MvcHtmlString(field.Name);} }return new MvcHtmlString(string.Format("{0}",claimType.Split('/', '.').Last()));} }} Note The helper method isn’t at all efficient because it reflects on the fields of the ClaimType class for each claim that is displayed, but it is sufficient for my purposes in this chapter. You won’t often need to display the claim type in real applications. 注:该辅助器并非十分有效,因为它只是针对每个要显示的声明(Claim)映射出ClaimType类的字段,但对我要的目的已经足够了。在实际项目中不会经常需要显示声明(Claim)的类型。 To see why I have created a controller that uses claims without really explaining what they are, start the application, authenticate as the user Alice (with the password MySecret), and request the /Claims/Index URL. Figure 15-5 shows the content that is generated. 为了弄明白我为何要先创建一个使用声明(Claims)的控制器,而没有真正解释声明(Claims)是什么的原因,可以启动应用程序,以用户Alice进行认证(其口令是MySecret),并请求/Claims/Index URL。图15-5显示了生成的内容。 Figure 15-5. The output from the Index action of the Claims controller 图15-5. Claims控制器中Index动作的输出 It can be hard to make out the detail in the figure, so I have reproduced the content in Table 15-7. 这可能还难以认识到此图的细节,为此我在表15-7中重列了其内容。 Table 15-7. The Data Shown in Figure 15-5 表15-7. 图15-5中显示的数据 Subject(科目) Issuer(发行者) Type(类型) Value(值) Alice LOCAL AUTHORITY SecurityStamp Unique ID Alice LOCAL AUTHORITY IdentityProvider ASP.NET Identity Alice LOCAL AUTHORITY Role Employees Alice LOCAL AUTHORITY Role Users Alice LOCAL AUTHORITY Name Alice Alice LOCAL AUTHORITY NameIdentifier Alice’s user ID The table shows the most important aspect of claims, which is that I have already been using them when I implemented the traditional authentication and authorization features in Chapter 14. You can see that some of the claims relate to user identity (the Name claim is Alice, and the NameIdentifier claim is Alice’s unique user ID in my ASP.NET Identity database). 此表展示了声明(Claims)最重要的方面,这些是我在第14章中实现传统的认证和授权特性时,一直在使用的信息。可以看出,有些声明(Claims)与用户标识有关(Name声明是Alice,NameIdentifier声明是Alice在ASP.NET Identity数据库中的唯一用户ID号)。 Other claims show membership of roles—there are two Role claims in the table, reflecting the fact that Alice is assigned to both the Users and Employees roles. There is also a claim about how Alice has been authenticated: The IdentityProvider is set to ASP.NET Identity. 其他声明(Claims)显示了角色成员——表中有两个Role声明(Claim),体现出Alice被赋予了Users和Employees两个角色这一事实。还有一个是Alice已被认证的声明(Claim):IdentityProvider被设置到了ASP.NET Identity。 The difference when this information is expressed as a set of claims is that you can determine where the data came from. The Issuer property for all the claims shown in the table is set to LOCAL AUTHORITY, which indicates that the user’s identity has been established by the application. 当这种信息被表示成一组声明(Claims)时的差别是,你能够确定这些数据是从哪里来的。表中所显示的所有声明的Issuer属性(发布者)都被设置到了LOACL AUTHORITY(本地授权),这说明该用户的标识是由应用程序建立的。 So, now that you have seen some example claims, I can more easily describe what a claim is. A claim is any piece of information about a user that is available to the application, including the user’s identity and role memberships. And, as you have seen, the information I have been defining about my users in earlier chapters is automatically made available as claims by ASP.NET Identity. 因此,现在你已经看到了一些声明(Claims)示例,我可以更容易地描述声明(Claim)是什么了。一项声明(Claim)是可用于应用程序中的有关用户的一个信息片段,包括用户的标识以及角色成员等。而且,正如你所看到的,我在前几章定义的关于用户的信息,被ASP.NET Identity自动地作为声明(Claims)了。 15.3.2 Creating and Using Claims 15.3.2 创建和使用声明(Claims) Claims are interesting for two reasons. The first reason is that an application can obtain claims from multiple sources, rather than just relying on a local database for information about the user. You will see a real example of this when I show you how to authenticate users through a third-party system in the “Using Third-Party Authentication” section, but for the moment I am going to add a class to the example project that simulates a system that provides claims information. Listing 15-15 shows the contents of the LocationClaimsProvider.cs file that I added to the Infrastructure folder. 声明(Claims)比较有意思的原因有两个。第一个原因是应用程序可以从多个来源获取声明(Claims),而不是只能依靠本地数据库关于用户的信息。你将会看到一个实际的示例,在“使用第三方认证”小节中,将演示如何通过第三方系统来认证用户。不过,此刻我只打算在示例项目中添加一个类,用以模拟一个提供声明(Claims)信息的系统。清单15-15显示了我添加到Infrastructure文件夹中LocationClaimsProvider.cs文件的内容。 Listing 15-15. The Contents of the LocationClaimsProvider.cs File 清单15-15. LocationClaimsProvider.cs文件的内容 using System.Collections.Generic;using System.Security.Claims; namespace Users.Infrastructure {public static class LocationClaimsProvider {public static IEnumerable<Claim> GetClaims(ClaimsIdentity user) {List<Claim> claims = new List<Claim>();if (user.Name.ToLower() == "alice") {claims.Add(CreateClaim(ClaimTypes.PostalCode, "DC 20500"));claims.Add(CreateClaim(ClaimTypes.StateOrProvince, "DC"));} else {claims.Add(CreateClaim(ClaimTypes.PostalCode, "NY 10036"));claims.Add(CreateClaim(ClaimTypes.StateOrProvince, "NY"));}return claims;}private static Claim CreateClaim(string type, string value) {return new Claim(type, value, ClaimValueTypes.String, "RemoteClaims");} }} The GetClaims method takes a ClaimsIdentity argument and uses the Name property to create claims about the user’s ZIP code and state. This class allows me to simulate a system such as a central HR database, which would be the authoritative source of location information about staff, for example. GetClaims方法以ClaimsIdentity为参数,并使用Name属性创建了关于用户ZIP码(邮政编码)和州府的声明(Claims)。上述这个类使我能够模拟一个诸如中心化的HR数据库(人力资源数据库)之类的系统,它可能会成为全体职员的地点信息的权威数据源。 Claims are associated with the user’s identity during the authentication process, and Listing 15-16 shows the changes I made to the Login action method of the Account controller to call the LocationClaimsProvider class. 在认证过程期间,声明(Claims)是与用户标识关联在一起的,清单15-16显示了我对Account控制器中Login动作方法所做的修改,以便调用LocationClaimsProvider类。 Listing 15-16. Associating Claims with a User in the AccountController.cs File 清单15-16. AccountController.cs文件中用户用声明的关联 ...[HttpPost][AllowAnonymous][ValidateAntiForgeryToken]public async Task<ActionResult> Login(LoginModel details, string returnUrl) {if (ModelState.IsValid) {AppUser user = await UserManager.FindAsync(details.Name,details.Password);if (user == null) {ModelState.AddModelError("", "Invalid name or password.");} else {ClaimsIdentity ident = await UserManager.CreateIdentityAsync(user,DefaultAuthenticationTypes.ApplicationCookie); ident.AddClaims(LocationClaimsProvider.GetClaims(ident));AuthManager.SignOut();AuthManager.SignIn(new AuthenticationProperties {IsPersistent = false}, ident);return Redirect(returnUrl);} }ViewBag.returnUrl = returnUrl;return View(details);}... You can see the effect of the location claims by starting the application, authenticating as a user, and requesting the /Claim/Index URL. Figure 15-6 shows the claims for Alice. You may have to sign out and sign back in again to see the change. 为了看看这个地点声明(Claims)的效果,可以启动应用程序,以一个用户进行认证,并请求/Claim/Index URL。图15-6显示了Alice的声明(Claims)。你可能需要退出,然后再次登录才会看到发生的变化。 Figure 15-6. Defining additional claims for users 图15-6. 定义用户的附加声明 Obtaining claims from multiple locations means that the application doesn’t have to duplicate data that is held elsewhere and allows integration of data from external parties. The Claim.Issuer property tells you where a claim originated from, which helps you judge how accurate the data is likely to be and how much weight you should give the data in your application. Location data obtained from a central HR database is likely to be more accurate and trustworthy than data obtained from an external mailing list provider, for example. 从多个地点获取声明(Claims)意味着应用程序不必复制其他地方保持的数据,并且能够与外部的数据集成。Claim.Issuer属性(图15-6中的Issuer数据列——译者注)能够告诉你一个声明(Claim)的发源地,这有助于让你判断数据的精确程度,也有助于让你决定这类数据在应用程序中的权重。例如,从中心化的HR数据库获取的地点数据可能要比外部邮件列表提供器获取的数据更为精确和可信。 1. Applying Claims 1. 运用声明(Claims) The second reason that claims are interesting is that you can use them to manage user access to your application more flexibly than with standard roles. The problem with roles is that they are static, and once a user has been assigned to a role, the user remains a member until explicitly removed. This is, for example, how long-term employees of big corporations end up with incredible access to internal systems: They are assigned the roles they require for each new job they get, but the old roles are rarely removed. (The unexpectedly broad systems access sometimes becomes apparent during the investigation into how someone was able to ship the contents of the warehouse to their home address—true story.) 声明(Claims)有意思的第二个原因是,你可以用它们来管理用户对应用程序的访问,这要比标准的角色管理更为灵活。角色的问题在于它们是静态的,而且一旦用户已经被赋予了一个角色,该用户便是一个成员,直到明确地删除为止。例如,这意味着大公司的长期雇员,对内部系统的访问会十分惊人:他们每次在获得新工作时,都会赋予所需的角色,但旧角色很少被删除。(在调查某人为何能够将仓库里的东西发往他的家庭地址过程中发现,有时会出现异常宽泛的系统访问——真实的故事) Claims can be used to authorize users based directly on the information that is known about them, which ensures that the authorization changes when the data changes. The simplest way to do this is to generate Role claims based on user data that are then used by controllers to restrict access to action methods. Listing 15-17 shows the contents of the ClaimsRoles.cs file that I added to the Infrastructure. 声明(Claims)可以直接根据用户已知的信息对用户进行授权,这能够保证当数据发生变化时,授权也随之而变。此事最简单的做法是根据用户数据来生成Role声明(Claim),然后由控制器用来限制对动作方法的访问。清单15-17显示了我添加到Infrastructure中的ClaimsRoles.cs文件的内容。 Listing 15-17. The Contents of the ClaimsRoles.cs File 清单15-17. ClaimsRoles.cs文件的内容 using System.Collections.Generic;using System.Security.Claims; namespace Users.Infrastructure {public class ClaimsRoles {public static IEnumerable<Claim> CreateRolesFromClaims(ClaimsIdentity user) {List<Claim> claims = new List<Claim>();if (user.HasClaim(x => x.Type == ClaimTypes.StateOrProvince&& x.Issuer == "RemoteClaims" && x.Value == "DC")&& user.HasClaim(x => x.Type == ClaimTypes.Role&& x.Value == "Employees")) {claims.Add(new Claim(ClaimTypes.Role, "DCStaff"));}return claims;} }} The gnarly looking CreateRolesFromClaims method uses lambda expressions to determine whether the user has a StateOrProvince claim from the RemoteClaims issuer with a value of DC and a Role claim with a value of Employees. If the user has both claims, then a Role claim is returned for the DCStaff role. Listing 15-18 shows how I call the CreateRolesFromClaims method from the Login action in the Account controller. CreateRolesFromClaims是一个粗糙的考察方法,它使用了Lambda表达式,以检查用户是否具有StateOrProvince声明(Claim),该声明来自于RemoteClaims发行者(Issuer),值为DC。也检查用户是否具有Role声明(Claim),其值为Employees。如果用户这两个声明都有,那么便返回一个DCStaff角色的Role声明。清单15-18显示了如何在Account控制器中的Login动作中调用CreateRolesFromClaims方法。 Listing 15-18. Generating Roles Based on Claims in the AccountController.cs File 清单15-18. 在AccountController.cs中根据声明生成角色 ...[HttpPost][AllowAnonymous][ValidateAntiForgeryToken]public async Task<ActionResult> Login(LoginModel details, string returnUrl) {if (ModelState.IsValid) {AppUser user = await UserManager.FindAsync(details.Name,details.Password);if (user == null) {ModelState.AddModelError("", "Invalid name or password.");} else {ClaimsIdentity ident = await UserManager.CreateIdentityAsync(user,DefaultAuthenticationTypes.ApplicationCookie);ident.AddClaims(LocationClaimsProvider.GetClaims(ident)); ident.AddClaims(ClaimsRoles.CreateRolesFromClaims(ident));AuthManager.SignOut();AuthManager.SignIn(new AuthenticationProperties {IsPersistent = false}, ident);return Redirect(returnUrl);} }ViewBag.returnUrl = returnUrl;return View(details);}... I can then restrict access to an action method based on membership of the DCStaff role. Listing 15-19 shows a new action method I added to the Claims controller to which I have applied the Authorize attribute. 然后我可以根据DCStaff角色的成员,来限制对一个动作方法的访问。清单15-19显示了在Claims控制器中添加的一个新的动作方法,在该方法上已经运用了Authorize注解属性。 Listing 15-19. Adding a New Action Method to the ClaimsController.cs File 清单15-19. 在ClaimsController.cs文件中添加一个新的动作方法 using System.Security.Claims;using System.Web;using System.Web.Mvc;namespace Users.Controllers {public class ClaimsController : Controller {[Authorize]public ActionResult Index() {ClaimsIdentity ident = HttpContext.User.Identity as ClaimsIdentity;if (ident == null) {return View("Error", new string[] { "No claims available" });} else {return View(ident.Claims);} } [Authorize(Roles="DCStaff")]public string OtherAction() {return "This is the protected action";} }} Users will be able to access OtherAction only if their claims grant them membership to the DCStaff role. Membership of this role is generated dynamically, so a change to the user’s employment status or location information will change their authorization level. 只要用户的声明(Claims)承认他们是DCStaff角色的成员,那么他们便能访问OtherAction动作。该角色的成员是动态生成的,因此,若是用户的雇用状态或地点信息发生变化,也会改变他们的授权等级。 提示:请读者从这个例子中吸取其中的思想精髓。对于读物的理解程度,仁者见仁,智者见智,能领悟多少,全凭各人,译者感觉这里的思想有无数的可能。举例说明:(1)可以根据用户的身份进行授权,比如学生在校时是“学生”,毕业后便是“校友”;(2)可以根据用户所处的部门进行授权,人事部用户属于人事团队,销售部用户属于销售团队,各团队有其自己的应用;(3)下一小节的示例是根据用户的地点授权。简言之:一方面用户的各种声明(Claim)都可以用来进行授权;另一方面用户的声明(Claim)又是可以自定义的。于是可能的运用就无法估计了。总之一句话,这种基于声明的授权(Claims-Based Authorization)有无限可能!要是没有我这里的提示,是否所有读者在此处都会有所体会?——译者注 15.3.3 Authorizing Access Using Claims 15.3.3 使用声明(Claims)授权访问 The previous example is an effective demonstration of how claims can be used to keep authorizations fresh and accurate, but it is a little indirect because I generate roles based on claims data and then enforce my authorization policy based on the membership of that role. A more direct and flexible approach is to enforce authorization directly by creating a custom authorization filter attribute. Listing 15-20 shows the contents of the ClaimsAccessAttribute.cs file, which I added to the Infrastructure folder and used to create such a filter. 前面的示例有效地演示了如何用声明(Claims)来保持新鲜和准确的授权,但有点不太直接,因为我要根据声明(Claims)数据来生成了角色,然后强制我的授权策略基于角色成员。一个更直接且灵活的办法是直接强制授权,其做法是创建一个自定义的授权过滤器注解属性。清单15-20演示了ClaimsAccessAttribute.cs文件的内容,我将它添加在Infrastructure文件夹中,并用它创建了这种过滤器。 Listing 15-20. The Contents of the ClaimsAccessAttribute.cs File 清单15-20. ClaimsAccessAttribute.cs文件的内容 using System.Security.Claims;using System.Web;using System.Web.Mvc; namespace Users.Infrastructure {public class ClaimsAccessAttribute : AuthorizeAttribute {public string Issuer { get; set; }public string ClaimType { get; set; }public string Value { get; set; }protected override bool AuthorizeCore(HttpContextBase context) {return context.User.Identity.IsAuthenticated&& context.User.Identity is ClaimsIdentity&& ((ClaimsIdentity)context.User.Identity).HasClaim(x =>x.Issuer == Issuer && x.Type == ClaimType && x.Value == Value);} }} The attribute I have defined is derived from the AuthorizeAttribute class, which makes it easy to create custom authorization policies in MVC framework applications by overriding the AuthorizeCore method. My implementation grants access if the user is authenticated, the IIdentity implementation is an instance of ClaimsIdentity, and the user has a claim with the issuer, type, and value matching the class properties. Listing 15-21 shows how I applied the attribute to the Claims controller to authorize access to the OtherAction method based on one of the location claims created by the LocationClaimsProvider class. 我所定义的这个注解属性派生于AuthorizeAttribute类,通过重写AuthorizeCore方法,很容易在MVC框架应用程序中创建自定义的授权策略。在这个实现中,若用户是已认证的、其IIdentity实现是一个ClaimsIdentity实例,而且该用户有一个带有issuer、type以及value的声明(Claim),它们与这个类的属性是匹配的,则该用户便是允许访问的。清单15-21显示了如何将这个注解属性运用于Claims控制器,以便根据LocationClaimsProvider类创建的地点声明(Claim),对OtherAction方法进行授权访问。 Listing 15-21. Performing Authorization on Claims in the ClaimsController.cs File 清单15-21. 在ClaimsController.cs文件中执行基于声明的授权 using System.Security.Claims;using System.Web;using System.Web.Mvc;using Users.Infrastructure;namespace Users.Controllers {public class ClaimsController : Controller {[Authorize]public ActionResult Index() {ClaimsIdentity ident = HttpContext.User.Identity as ClaimsIdentity;if (ident == null) {return View("Error", new string[] { "No claims available" });} else {return View(ident.Claims);} } [ClaimsAccess(Issuer="RemoteClaims", ClaimType=ClaimTypes.PostalCode,Value="DC 20500")]public string OtherAction() {return "This is the protected action";} }} My authorization filter ensures that only users whose location claims specify a ZIP code of DC 20500 can invoke the OtherAction method. 这个授权过滤器能够确保只有地点声明(Claim)的邮编为DC 20500的用户才能请求OtherAction方法。 15.4 Using Third-Party Authentication 15.4 使用第三方认证 One of the benefits of a claims-based system such as ASP.NET Identity is that any of the claims can come from an external system, even those that identify the user to the application. This means that other systems can authenticate users on behalf of the application, and ASP.NET Identity builds on this idea to make it simple and easy to add support for authenticating users through third parties such as Microsoft, Google, Facebook, and Twitter. 基于声明的系统,如ASP.NET Identity,的好处之一是任何声明都可以来自于外部系统,即使是将用户标识到应用程序的那些声明。这意味着其他系统可以代表应用程序来认证用户,而ASP.NET Identity就建立在这样的思想之上,使之能够简单而方便地添加第三方认证用户的支持,如微软、Google、Facebook、Twitter等。 There are some substantial benefits of using third-party authentication: Many users will already have an account, users can elect to use two-factor authentication, and you don’t have to manage user credentials in the application. In the sections that follow, I’ll show you how to set up and use third-party authentication for Google users, which Table 15-8 puts into context. 使用第三方认证有一些实际的好处:许多用户已经有了账号、用户可以选择使用双因子认证、你不必在应用程序中管理用户凭据等等。在以下小节中,我将演示如何为Google用户建立并使用第三方认证,表15-8描述了事情的情形。 Table 15-8. Putting Third-Party Authentication in Context 表15-8. 第三方认证情形 Question 问题 Answer 回答 What is it? 什么是第三方认证? Authenticating with third parties lets you take advantage of the popularity of companies such as Google and Facebook. 第三方认证使你能够利用流行公司,如Google和Facebook,的优势。 Why should I care? 为何要关心它? Users don’t like having to remember passwords for many different sites. Using a provider with large-scale adoption can make your application more appealing to users of the provider’s services. 用户不喜欢记住许多不同网站的口令。使用大范围适应的提供器可使你的应用程序更吸引有提供器服务的用户。 How is it used by the MVC framework? 如何在MVC框架中使用它? This feature isn’t used directly by the MVC framework. 这不是一个直接由MVC框架使用的特性。 Note The reason I have chosen to demonstrate Google authentication is that it is the only option that doesn’t require me to register my application with the authentication service. You can get details of the registration processes required at http://bit.ly/1cqLTrE. 提示:我选择演示Google认证的原因是,它是唯一不需要在其认证服务中注册我应用程序的公司。有关认证服务注册过程的细节,请参阅http://bit.ly/1cqLTrE。 15.4.1 Enabling Google Authentication 15.4.1 启用Google认证 ASP.NET Identity comes with built-in support for authenticating users through their Microsoft, Google, Facebook, and Twitter accounts as well more general support for any authentication service that supports OAuth. The first step is to add the NuGet package that includes the Google-specific additions for ASP.NET Identity. Enter the following command into the Package Manager Console: ASP.NET Identity带有通过Microsoft、Google、Facebook以及Twitter账号认证用户的内建支持,并且对于支持OAuth的认证服务具有更普遍的支持。第一个步骤是添加NuGet包,包中含有用于ASP.NET Identity的Google专用附件。请在“Package Manager Console(包管理器控制台)”中输入以下命令: Install-Package Microsoft.Owin.Security.Google -version 2.0.2 There are NuGet packages for each of the services that ASP.NET Identity supports, as described in Table 15-9. 对于ASP.NET Identity支持的每一种服务都有相应的NuGet包,如表15-9所示。 Table 15-9. The NuGet Authenticaton Packages 表15-9. NuGet认证包 Name 名称 Description 描述 Microsoft.Owin.Security.Google Authenticates users with Google accounts 用Google账号认证用户 Microsoft.Owin.Security.Facebook Authenticates users with Facebook accounts 用Facebook账号认证用户 Microsoft.Owin.Security.Twitter Authenticates users with Twitter accounts 用Twitter账号认证用户 Microsoft.Owin.Security.MicrosoftAccount Authenticates users with Microsoft accounts 用Microsoft账号认证用户 Microsoft.Owin.Security.OAuth Authenticates users against any OAuth 2.0 service 根据任一OAuth 2.0服务认证用户 Once the package is installed, I enable support for the authentication service in the OWIN startup class, which is defined in the App_Start/IdentityConfig.cs file in the example project. Listing 15-22 shows the change that I have made. 一旦安装了这个包,便可以在OWIN启动类中启用此项认证服务的支持,启动类的定义在示例项目的App_Start/IdentityConfig.cs文件中。清单15-22显示了所做的修改。 Listing 15-22. Enabling Google Authentication in the IdentityConfig.cs File 清单15-22. 在IdentityConfig.cs文件中启用Google认证 using Microsoft.AspNet.Identity;using Microsoft.Owin;using Microsoft.Owin.Security.Cookies;using Owin;using Users.Infrastructure;using Microsoft.Owin.Security.Google;namespace Users {public class IdentityConfig {public void Configuration(IAppBuilder app) {app.CreatePerOwinContext<AppIdentityDbContext>(AppIdentityDbContext.Create);app.CreatePerOwinContext<AppUserManager>(AppUserManager.Create);app.CreatePerOwinContext<AppRoleManager>(AppRoleManager.Create); app.UseCookieAuthentication(new CookieAuthenticationOptions {AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,LoginPath = new PathString("/Account/Login"),}); app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);app.UseGoogleAuthentication();} }} Each of the packages that I listed in Table 15-9 contains an extension method that enables the corresponding service. The extension method for the Google service is called UseGoogleAuthentication, and it is called on the IAppBuilder implementation that is passed to the Configuration method. 表15-9所列的每个包都含有启用相应服务的扩展方法。用于Google服务的扩展方法名称为UseGoogleAuthentication,它通过传递给Configuration方法的IAppBuilder实现进行调用。 Next I added a button to the Views/Account/Login.cshtml file, which allows users to log in via Google. You can see the change in Listing 15-23. 下一步骤是在Views/Account/Login.cshtml文件中添加一个按钮,让用户能够通过Google进行登录。所做的修改如清单15-23所示。 Listing 15-23. Adding a Google Login Button to the Login.cshtml File 清单15-23. 在Login.cshtml文件中添加Google登录按钮 @model Users.Models.LoginModel@{ ViewBag.Title = "Login";}<h2>Log In</h2> @Html.ValidationSummary()@using (Html.BeginForm()) {@Html.AntiForgeryToken();<input type="hidden" name="returnUrl" value="@ViewBag.returnUrl" /><div class="form-group"><label>Name</label>@Html.TextBoxFor(x => x.Name, new { @class = "form-control" })</div><div class="form-group"><label>Password</label>@Html.PasswordFor(x => x.Password, new { @class = "form-control" })</div><button class="btn btn-primary" type="submit">Log In</button>}@using (Html.BeginForm("GoogleLogin", "Account")) {<input type="hidden" name="returnUrl" value="@ViewBag.returnUrl" /><button class="btn btn-primary" type="submit">Log In via Google</button>} The new button submits a form that targets the GoogleLogin action on the Account controller. You can see this method—and the other changes I made the controller—in Listing 15-24. 新按钮递交一个表单,目标是Account控制器中的GoogleLogin动作。可从清单15-24中看到该方法,以及在控制器中所做的其他修改。 Listing 15-24. Adding Support for Google Authentication to the AccountController.cs File 清单15-24. 在AccountController.cs文件中添加Google认证支持 using System.Threading.Tasks;using System.Web.Mvc;using Users.Models;using Microsoft.Owin.Security;using System.Security.Claims;using Microsoft.AspNet.Identity;using Microsoft.AspNet.Identity.Owin;using Users.Infrastructure;using System.Web; namespace Users.Controllers {[Authorize]public class AccountController : Controller {[AllowAnonymous]public ActionResult Login(string returnUrl) {if (HttpContext.User.Identity.IsAuthenticated) {return View("Error", new string[] { "Access Denied" });}ViewBag.returnUrl = returnUrl;return View();}[HttpPost][AllowAnonymous][ValidateAntiForgeryToken]public async Task<ActionResult> Login(LoginModel details, string returnUrl) {if (ModelState.IsValid) {AppUser user = await UserManager.FindAsync(details.Name,details.Password);if (user == null) {ModelState.AddModelError("", "Invalid name or password.");} else {ClaimsIdentity ident = await UserManager.CreateIdentityAsync(user,DefaultAuthenticationTypes.ApplicationCookie); ident.AddClaims(LocationClaimsProvider.GetClaims(ident));ident.AddClaims(ClaimsRoles.CreateRolesFromClaims(ident)); AuthManager.SignOut();AuthManager.SignIn(new AuthenticationProperties {IsPersistent = false}, ident);return Redirect(returnUrl);} }ViewBag.returnUrl = returnUrl;return View(details);} [HttpPost][AllowAnonymous]public ActionResult GoogleLogin(string returnUrl) {var properties = new AuthenticationProperties {RedirectUri = Url.Action("GoogleLoginCallback",new { returnUrl = returnUrl})};HttpContext.GetOwinContext().Authentication.Challenge(properties, "Google");return new HttpUnauthorizedResult();}[AllowAnonymous]public async Task<ActionResult> GoogleLoginCallback(string returnUrl) {ExternalLoginInfo loginInfo = await AuthManager.GetExternalLoginInfoAsync();AppUser user = await UserManager.FindAsync(loginInfo.Login);if (user == null) {user = new AppUser {Email = loginInfo.Email,UserName = loginInfo.DefaultUserName,City = Cities.LONDON, Country = Countries.UK};IdentityResult result = await UserManager.CreateAsync(user);if (!result.Succeeded) {return View("Error", result.Errors);} else {result = await UserManager.AddLoginAsync(user.Id, loginInfo.Login);if (!result.Succeeded) {return View("Error", result.Errors);} }}ClaimsIdentity ident = await UserManager.CreateIdentityAsync(user,DefaultAuthenticationTypes.ApplicationCookie);ident.AddClaims(loginInfo.ExternalIdentity.Claims);AuthManager.SignIn(new AuthenticationProperties {IsPersistent = false }, ident);return Redirect(returnUrl ?? "/");}[Authorize]public ActionResult Logout() {AuthManager.SignOut();return RedirectToAction("Index", "Home");}private IAuthenticationManager AuthManager {get {return HttpContext.GetOwinContext().Authentication;} }private AppUserManager UserManager {get {return HttpContext.GetOwinContext().GetUserManager<AppUserManager>();} }} } The GoogleLogin method creates an instance of the AuthenticationProperties class and sets the RedirectUri property to a URL that targets the GoogleLoginCallback action in the same controller. The next part is a magic phrase that causes ASP.NET Identity to respond to an unauthorized error by redirecting the user to the Google authentication page, rather than the one defined by the application: GoogleLogin方法创建了AuthenticationProperties类的一个实例,并为RedirectUri属性设置了一个URL,其目标为同一控制器中的GoogleLoginCallback动作。下一个部分是一个神奇阶段,通过将用户重定向到Google认证页面,而不是应用程序所定义的认证页面,让ASP.NET Identity对未授权的错误进行响应: ...HttpContext.GetOwinContext().Authentication.Challenge(properties, "Google");return new HttpUnauthorizedResult();... This means that when the user clicks the Log In via Google button, their browser is redirected to the Google authentication service and then redirected back to the GoogleLoginCallback action method once they are authenticated. 这意味着,当用户通过点击Google按钮进行登录时,浏览器被重定向到Google的认证服务,一旦在那里认证之后,便被重定向回GoogleLoginCallback动作方法。 I get details of the external login by calling the GetExternalLoginInfoAsync of the IAuthenticationManager implementation, like this: 我通过调用IAuthenticationManager实现的GetExternalLoginInfoAsync方法,我获得了外部登录的细节,如下所示: ...ExternalLoginInfo loginInfo = await AuthManager.GetExternalLoginInfoAsync();... The ExternalLoginInfo class defines the properties shown in Table 15-10. ExternalLoginInfo类定义的属性如表15-10所示: Table 15-10. The Properties Defined by the ExternalLoginInfo Class 表15-10. ExternalLoginInfo类所定义的属性 Name 名称 Description 描述 DefaultUserName Returns the username 返回用户名 Email Returns the e-mail address 返回E-mail地址 ExternalIdentity Returns a ClaimsIdentity that identities the user 返回标识该用户的ClaimsIdentity Login Returns a UserLoginInfo that describes the external login 返回描述外部登录的UserLoginInfo I use the FindAsync method defined by the user manager class to locate the user based on the value of the ExternalLoginInfo.Login property, which returns an AppUser object if the user has been authenticated with the application before: 我使用了由用户管理器类所定义的FindAsync方法,以便根据ExternalLoginInfo.Login属性的值对用户进行定位,如果用户之前在应用程序中已经认证,该属性会返回一个AppUser对象: ...AppUser user = await UserManager.FindAsync(loginInfo.Login);... If the FindAsync method doesn’t return an AppUser object, then I know that this is the first time that this user has logged into the application, so I create a new AppUser object, populate it with values, and save it to the database. I also save details of how the user logged in so that I can find them next time: 如果FindAsync方法返回的不是AppUser对象,那么我便知道这是用户首次登录应用程序,于是便创建了一个新的AppUser对象,填充该对象的值,并将其保存到数据库。我还保存了用户如何登录的细节,以便下次能够找到他们: ...result = await UserManager.AddLoginAsync(user.Id, loginInfo.Login);... All that remains is to generate an identity the user, copy the claims provided by Google, and create an authentication cookie so that the application knows the user has been authenticated: 剩下的事情只是生成该用户的标识了,拷贝Google提供的声明(Claims),并创建一个认证Cookie,以使应用程序知道此用户已认证: ...ClaimsIdentity ident = await UserManager.CreateIdentityAsync(user,DefaultAuthenticationTypes.ApplicationCookie);ident.AddClaims(loginInfo.ExternalIdentity.Claims);AuthManager.SignIn(new AuthenticationProperties { IsPersistent = false }, ident);... 15.4.2 Testing Google Authentication 15.4.2 测试Google认证 There is one further change that I need to make before I can test Google authentication: I need to change the account verification I set up in Chapter 13 because it prevents accounts from being created with e-mail addresses that are not within the example.com domain. Listing 15-25 shows how I removed the verification from the AppUserManager class. 在测试Google认证之前还需要一处修改:需要修改第13章所建立的账号验证,因为它不允许example.com域之外的E-mail地址创建账号。清单15-25显示了如何在AppUserManager类中删除这种验证。 Listing 15-25. Disabling Account Validation in the AppUserManager.cs File 清单15-25. 在AppUserManager.cs文件中取消账号验证 using Microsoft.AspNet.Identity;using Microsoft.AspNet.Identity.EntityFramework;using Microsoft.AspNet.Identity.Owin;using Microsoft.Owin;using Users.Models; namespace Users.Infrastructure {public class AppUserManager : UserManager<AppUser> {public AppUserManager(IUserStore<AppUser> store): base(store) {}public static AppUserManager Create(IdentityFactoryOptions<AppUserManager> options,IOwinContext context) {AppIdentityDbContext db = context.Get<AppIdentityDbContext>();AppUserManager manager = new AppUserManager(new UserStore<AppUser>(db)); manager.PasswordValidator = new CustomPasswordValidator {RequiredLength = 6,RequireNonLetterOrDigit = false,RequireDigit = false,RequireLowercase = true,RequireUppercase = true}; //manager.UserValidator = new CustomUserValidator(manager) {// AllowOnlyAlphanumericUserNames = true,// RequireUniqueEmail = true//};return manager;} }} Tip you can use validation for externally authenticated accounts, but I am just going to disable the feature for simplicity. 提示:也可以使用外部已认证账号的验证,但这里出于简化,取消了这一特性。 To test authentication, start the application, click the Log In via Google button, and provide the credentials for a valid Google account. When you have completed the authentication process, your browser will be redirected back to the application. If you navigate to the /Claims/Index URL, you will be able to see how claims from the Google system have been added to the user’s identity, as shown in Figure 15-7. 为了测试认证,启动应用程序,通过点击“Log In via Google(通过Google登录)”按钮,并提供有效的Google账号凭据。当你完成了认证过程时,浏览器将被重定向回应用程序。如果导航到/Claims/Index URL,便能够看到来自Google系统的声明(Claims),已被添加到用户的标识中了,如图15-7所示。 Figure 15-7. Claims from Google 图15-7. 来自Google的声明(Claims) 15.5 Summary 15.5 小结 In this chapter, I showed you some of the advanced features that ASP.NET Identity supports. I demonstrated the use of custom user properties and how to use database migrations to preserve data when you upgrade the schema to support them. I explained how claims work and how they can be used to create more flexible ways of authorizing users. I finished the chapter by showing you how to authenticate users via Google, which builds on the ideas behind the use of claims. 本章向你演示了ASP.NET Identity所支持的一些高级特性。演示了自定义用户属性的使用,还演示了在升级数据架构时,如何使用数据库迁移保护数据。我解释了声明(Claims)的工作机制,以及如何将它们用于创建更灵活的用户授权方式。最后演示了如何通过Google进行认证结束了本章,这是建立在使用声明(Claims)的思想基础之上的。 本篇文章为转载内容。原文链接:https://blog.csdn.net/gz19871113/article/details/108591802。 该文由互联网用户投稿提供,文中观点代表作者本人意见,并不代表本站的立场。 作为信息平台,本站仅提供文章转载服务,并不拥有其所有权,也不对文章内容的真实性、准确性和合法性承担责任。 如发现本文存在侵权、违法、违规或事实不符的情况,请及时联系我们,我们将第一时间进行核实并删除相应内容。
2023-10-28 08:49:21
283
转载
转载文章
...会关注这一块。 漏洞技术概况 Pavel总结到,在阶段漏洞挖掘中共上报11个漏洞。在披露邮件中描述了其中4个的技术细节,均在协议数据包处理代码中,漏洞类型古典,分别是全局缓冲区溢出、堆溢出和空指针解引用。其中缓冲区溢出类型漏洞可方便构造PoC,实现远程任意代码执行的漏洞利用。 漏洞本身原理简单,也并不是关键。以其中一个为例,Pavel在发现时负责任地向LibVNC作者提交了issue,并跟进漏洞修复过程;在第一次修复之后,复核并指出修复代码无效,给出了有效patch。这个过程是常规操作。 漏洞疑点 有意思的是,在漏洞披露邮件中,Pavel重点谈了自己对这系列漏洞的一些周边发现,也是这里提到的原因。其中,关于存在漏洞的代码,作者表述: 我最初认为,这些问题是libvnc开发者自己代码中的错误,但看起来并非如此。其中有一些(如CoRRE数据处理函数中的堆缓冲区溢出),出现在AT&T实验室1999年的代码中,而后被很多软件开发者原样复制(在Github上搜索一下HandleCoRREBPP函数,你就知道),LibVNC和TightVNC也是如此。 为了证实,翻阅了这部分代码,确实在其中数据处理相关代码文件看到了剑桥和AT&T实验室的文件头GPL声明注释,中国菜刀 这证实这些文件是直接从最初剑桥实验室版本VNC移植过来的,且使用方式是 直接代码包含,而非独立库引用方式。在官方开源发布并停止更新后,LibVNC使用的这部分代码基本没有改动——除了少数变量命名方式的统一,以及本次漏洞修复。通过搜索,我找到了2000年发布的相关代码文件,确认这些文件与LibVNC中引入的原始版本一致。 另外,Pavel同时反馈了TightVNC中相同的问题。TightVNC与LibVNC没有继承和直接引用关系,但上述VNC代码同样被TightVNC使用,问题的模式不约而同。Pavel测试发现在Ubuntu最新版本TightVNC套件(1.3.10版本)中同样存在该问题,上报给当前软件所有者GlavSoft公司,但对方声称目前精力放在不受GPL限制的TightVNC 2.x版本开发中,对开源的1.x版本漏洞代码“可能会进行修复”。看起来,这个问题被踢给了各大Linux发行版社区来焦虑了——如果他们愿意接锅。 问题思考 在披露邮件中,Pavel认为,这些代码bug“如此明显,让人无法相信之前没被人发现过……也许是因为某些特殊理由才始终没得到修复”。 事实上,我们都知道目前存在一些对开源基础软件进行安全扫描的大型项目,例如Google的OSS;同时,仍然存活的开源项目也越来越注重自身代码发布前的安全扫描,Fortify、Coverity的扫描也成为很多项目和平台的标配。在这样一些眼睛注视下,为什么还有这样的问题?我认为就这个具体事例来说,可能有如下两个因素: ·上游已死。仍然在被维护的代码,存在版本更迭,也存在外界的持续关注、漏洞报告和修复、开发的迭代,对于负责人的开发者,持续跟进、评估、同步代码的改动是可能的。但是一旦一份代码走完了生命周期,就像一段史实一样会很少再被改动。 ·对第三方上游代码的无条件信任。我们很多人都有过基础组件、中间件的开发经历,不乏有人使用Coverity开启全部规则进行代码扫描、严格修复所有提示的问题甚至编程规范warning;报告往往很长,其中也包括有源码形式包含的第三方代码中的问题。但是,我们一方面倾向于认为这些被广泛使用的代码不应存在问题(不然早就被人挖过了),一方面考虑这些引用的代码往往是组件或库的形式被使用,应该有其上下文才能认定是否确实有可被利用的漏洞条件,现在单独扫描这部分代码一般出来的都是误报。所以这些代码的问题都容易被忽视。 但是透过这个具体例子,再延伸思考相关的实践,这里最根本的问题可以总结为一个模式: 复制粘贴风险。复制粘贴并不简单意味着剽窃,实际是当前软件领域、互联网行业发展的基础模式,但其中有一些没人能尝试解决的问题: ·在传统代码领域,如C代码中,对第三方代码功能的复用依赖,往往通过直接进行库的引入实现,第三方代码独立而完整,也较容易进行整体更新;这是最简单的情况,只需要所有下游使用者保证仅使用官方版本,跟进官方更新即可;但在实践中很难如此贯彻,这是下节讨论的问题。 ·有些第三方发布的代码,模式就是需要被源码形式包含到其他项目中进行统一编译使用(例如腾讯的开源Json解析库RapidJSON,就是纯C++头文件形式)。在开源领域有如GPL等规约对此进行规范,下游开发者遵循协议,引用代码,强制或可选地显式保留其GPL声明,可以进行使用和更改。这样的源码依赖关系,结合规范化的changelog声明代码改动,侧面也是为开发过程中跟进考虑。但是一个成型的产品,比如企业自有的服务端底层产品、中间件,新版本的发版更新是复杂的过程,开发者在旧版本仍然“功能正常”的情况下往往倾向于不跟进新版本;而上游代码如果进行安全漏洞修复,通常也都只在其最新版本代码中改动,安全修复与功能迭代并存,如果没有类似Linux发行版社区的努力,旧版本代码完全没有干净的安全更新patch可用。 ·在特定场景下,有些开发实践可能不严格遵循开源代码协议限定,引入了GPL等协议保护的代码而不做声明(以规避相关责任),丢失了引入和版本的信息跟踪;在另一些场景下,可能存在对开源代码进行大刀阔斧的修改、剪裁、定制,以符合自身业务的极端需求,但是过多的修改、人员的迭代造成与官方代码严重的失同步,丧失可维护性。 ·更一般的情况是,在开发中,开发者个体往往心照不宣的存在对网上代码文件、代码片段的复制-粘贴操作。被参考的代码,可能有上述的开源代码,也可能有各种Github作者练手项目、技术博客分享的代码片段、正式开源项目仅用来说明用法的不完备示例代码。这些代码的引入完全无迹可寻,即便是作者自己也很难解释用了什么。这种情况下,上面两条认定的那些与官方安全更新失同步的问题同样存在,且引入了独特的风险:被借鉴的代码可能只是原作者随手写的、仅仅是功能成立的片段,甚至可能是恶意作者随意散布的有安全问题的代码。由此,问题进入了最大的发散空间。 在Synopsys下BLACKDUCK软件之前发布的《2018 Open Source Security and Risk Analysis Report》中分析,96%的应用中包含有开源组件和代码,开源代码在应用全部代码中的占比约为57%,78%的应用中在引用的三方开源代码中存在历史漏洞。也就是说,现在互联网上所有厂商开发的软件、应用,其开发人员自己写的代码都是一少部分,多数都是借鉴来的。而这还只是可统计、可追溯的;至于上面提到的非规范的代码引用,如果也纳入进来考虑,三方代码占应用中的比例会上升到多少?曾经有分析认为至少占80%,我们只期望不会更高。 Ⅱ. 从碎片到乱刃:OpenSSH在野后门一览 在进行基础软件梳理时,回忆到反病毒安全软件提供商ESET在2018年十月发布的一份白皮书《THE DARK SIDE OF THE FORSSHE: A landscape of OpenSSH backdoors》。其站在一个具有广泛用户基础的软件提供商角度,给出了一份分析报告,数据和结论超出我们对于当前基础软件使用全景的估量。以下以我的角度对其中一方面进行解读。 一些必要背景 SSH的作用和重要性无需赘言;虽然我们站在传统互联网公司角度,可以认为SSH是通往生产服务器的生命通道,但当前多样化的产业环境已经不止于此(如之前libssh事件中,不幸被我言中的,SSH在网络设备、IoT设备上(如f5)的广泛使用)。 OpenSSH是目前绝大多数SSH服务端的基础软件,有完备的开发团队、发布规范、维护机制,本身是靠谱的。如同绝大多数基础软件开源项目的做法,OpenSSH对漏洞有及时的响应,针对最新版本代码发出安全补丁,但是各大Linux发行版使用的有各种版本的OpenSSH,这些社区自行负责将官方开发者的安全补丁移植到自己系统搭载的低版本代码上。天空彩 白皮书披露的现状 如果你是一个企业的运维管理人员,需要向企业生产服务器安装OpenSSH或者其它基础软件,最简单的方式当然是使用系统的软件管理安装即可。但是有时候,出于迁移成本考虑,可能企业需要在一个旧版本系统上,使用较新版本的OpenSSL、OpenSSH等基础软件,这些系统不提供,需要自行安装;或者需要一个某有种特殊特性的定制版本。这时,可能会选择从某些rpm包集中站下载某些不具名第三方提供的现成的安装包,或者下载非官方的定制化源码本地编译后安装,总之从这里引入了不确定性。 这种不确定性有多大?我们粗估一下,似乎不应成为问题。但这份白皮书给我们看到了鲜活的数据。 ESET研究人员从OpenSSH的一次历史大规模Linux服务端恶意软件Windigo中获得启示,采用某种巧妙的方式,面向在野的服务器进行数据采集,主要是系统与版本、安装的OpenSSH版本信息以及服务端程序文件的一个特殊签名。整理一个签名白名单,包含有所有能搜索到的官方发布二进制版本、各大Linux发行版本各个版本所带的程序文件版本,将这些标定为正常样本进行去除。最终结论是: ·共发现了几百个非白名单版本的OpenSSH服务端程序文件ssh和sshd; ·分析这些样本,将代码部分完全相同,仅仅是数据和配置不同的合并为一类,且分析判定确认有恶意代码的,共归纳为 21个各异的恶意OpenSSH家族; ·在21个恶意家族中,有12个家族在10月份时完全没有被公开发现分析过;而剩余的有一部分使用了历史上披露的恶意代码样本,甚至有源代码; ·所有恶意样本的实现,从实现复杂度、代码混淆和自我保护程度到代码特征有很大跨度的不同,但整体看,目的以偷取用户凭证等敏感信息、回连外传到攻击者为主,其中有的攻击者回连地址已经存在并活跃数年之久; ·这些后门的操控者,既有传统恶意软件黑产人员,也有APT组织; ·所有恶意软件或多或少都在被害主机上有未抹除的痕迹。ESET研究者尝试使用蜜罐引诱出攻击者,但仍有许多未解之谜。这场对抗,仍未取胜。 白皮书用了大篇幅做技术分析报告,此处供细节分析,不展开分析,以下为根据恶意程序复杂度描绘的21个家族图谱: 问题思考 问题引入的可能渠道,我在开头进行了一点推测,主要是由人的原因切入的,除此以外,最可能的是恶意攻击者在利用各种方法入侵目标主机后,主动替换了目标OpenSSH为恶意版本,从而达成攻击持久化操作。但是这些都是止血的安全运维人员该考虑的事情;关键问题是,透过表象,这显露了什么威胁形式? 这个问题很好回答,之前也曾经反复说过:基础软件碎片化。 如上一章节简单提到,在开发过程中有各种可能的渠道引入开发者不完全了解和信任的代码;在运维过程中也是如此。二者互相作用,造成了软件碎片化的庞杂现状。在企业内部,同一份基础软件库,可能不同的业务线各自定制一份,放到企业私有软件仓库源中,有些会有人持续更新供自己产品使用,有些由系统软件基础设施维护人员单独维护,有些则可能是开发人员临时想起来上传的,他们自己都不记得;后续用到的这个基础软件的开发和团队,在这个源上搜索到已有的库,很大概率会倾向于直接使用,不管来源、是否有质量背书等。长此以往问题会持续发酵。而我们开最坏的脑洞,是否可能有黑产人员入职到内部,提交个恶意基础库之后就走人的可能?现行企业安全开发流程中审核机制的普遍缺失给这留下了空位。 将源码来源碎片化与二进制使用碎片化并起来考虑,我们不难看到一个远远超过OpenSSH事件威胁程度的图景。但这个问题不是仅仅靠开发阶段规约、运维阶段规范、企业内部管控、行业自查、政府监管就可以根除的,最大的问题归根结底两句话: 不可能用一场战役对抗持续威胁;不可能用有限分析对抗无限未知。 Ⅲ. 从自信到自省:RHEL、CentOS backport版本BIND漏洞 2018年12月20日凌晨,在备战冬至的软件供应链安全大赛决赛时,我注意到漏洞预警平台捕获的一封邮件。但这不是一个漏洞初始披露邮件,而是对一个稍早已披露的BIND在RedHat、CentOS发行版上特定版本的1day漏洞CVE-2018-5742,由BIND的官方开发者进行额外信息澄(shuǎi)清(guō)的邮件。 一些必要背景 关于BIND 互联网的一个古老而基础的设施是DNS,这个概念在读者不应陌生。而BIND“是现今互联网上最常使用的DNS软件,使用BIND作为服务器软件的DNS服务器约占所有DNS服务器的九成。BIND现在由互联网系统协会负责开发与维护参考。”所以BIND的基础地位即是如此,因此也一向被大量白帽黑帽反复测试、挖掘漏洞,其开发者大概也一直处在紧绷着应对的处境。 关于ISC和RedHat 说到开发者,上面提到BIND的官方开发者是互联网系统协会(ISC)。ISC是一个老牌非营利组织,目前主要就是BIND和DHCP基础设施的维护者。而BIND本身如同大多数历史悠久的互联网基础开源软件,是4个UCB在校生在DARPA资助下于1984年的实验室产物,直到2012年由ISC接管。 那么RedHat在此中是什么角色呢?这又要提到我之前提到的Linux发行版和自带软件维护策略。Red Hat Enterprise Linux(RHEL)及其社区版CentOS秉持着稳健的软件策略,每个大的发行版本的软件仓库,都只选用最必要且质量久经时间考验的软件版本,哪怕那些版本实在是老掉牙。这不是一种过分的保守,事实证明这种策略往往给RedHat用户在最新漏洞面前提供了保障——代码总是跑得越少,潜在漏洞越多。 但是这有两个关键问题。一方面,如果开源基础软件被发现一例有历史沿革的代码漏洞,那么官方开发者基本都只为其最新代码负责,在当前代码上推出修复补丁。另一方面,互联网基础设施虽然不像其上的应用那样爆发性迭代,但依然持续有一些新特性涌现,其中一些是必不可少的,但同样只在最新代码中提供。两个刚需推动下,各Linux发行版对长期支持版本系统的软件都采用一致的策略,即保持其基础软件在一个固定的版本,但对于这些版本软件的最新漏洞、必要的最新软件特性,由发行版维护者将官方开发者最新代码改动“向后移植”到旧版本代码中,即backport。这就是基础软件的“官宣”碎片化的源头。 讲道理,Linux发行版维护者与社区具有比较靠谱的开发能力和监督机制,backport又基本就是一些复制粘贴工作,应当是很稳当的……但真是如此吗? CVE-2018-5742漏洞概况 CVE-2018-5742是一个简单的缓冲区溢出类型漏洞,官方评定其漏洞等级moderate,认为危害不大,漏洞修复不积极,披露信息不多,也没有积极给出代码修复patch和新版本rpm包。因为该漏洞仅在设置DEBUG_LEVEL为10以上才会触发,由远程攻击者构造畸形请求造成BIND服务崩溃,在正常的生产环境几乎不可能具有危害,RedHat官方也只是给出了用户自查建议。 这个漏洞只出现在RHEL和CentOS版本7中搭载的BIND 9.9.4-65及之后版本。RedHat同ISC的声明中都证实,这个漏洞的引入原因,是RedHat在尝试将BIND 9.11版本2016年新增的NTA机制向后移植到RedHat 7系中固定搭载的BIND 9.9版本代码时,偶然的代码错误。NTA是DNS安全扩展(DNSSEC)中,用于在特定域关闭DNSSEC校验以避免不必要的校验失败的机制;但这个漏洞不需要对NTA本身有进一步了解。 漏洞具体分析 官方没有给出具体分析,但根据CentOS社区里先前有用户反馈的bug,我得以很容易还原漏洞链路并定位到根本原因。 若干用户共同反馈,其使用的BIND 9.9.4-RedHat-9.9.4-72.el7发生崩溃(coredump),并给出如下的崩溃时调用栈backtrace: 这个调用过程的逻辑为,在9 dns_message_logfmtpacket函数判断当前软件设置是否DEBUG_LEVEL大于10,若是,对用户请求数据包做日志记录,先后调用8 dns_message_totext、7 dns_message_sectiontotext、6 dns_master_rdatasettotext、5 rdataset_totext将请求进行按协议分解分段后写出。 由以上关键调用环节,联动RedHat在9.9.4版本BIND源码包中关于引入NTA特性的源码patch,进行代码分析,很快定位到问题产生的位置,在上述backtrace中的5,masterdump.c文件rdataset_totext函数。漏洞相关代码片段中,RedHat进行backport后,这里引入的代码为: 这里判断对于请求中的注释类型数据,直接通过isc_buffer_putstr宏对缓存进行操作,在BIND工程中自定义维护的缓冲区结构对象target上,附加一字节字符串(一个分号)。而漏洞就是由此产生:isc_buffer_putstr中不做缓冲区边界检查保证,这里在缓冲区已满情况下将造成off-by-one溢出,并触发了缓冲区实现代码中的assertion。 而ISC上游官方版本的代码在这里是怎么写的呢?找到ISC版本BIND 9.11代码,这里是这样的: 这里可以看到,官方代码在做同样的“附加一个分号”这个操作时,审慎的使用了做缓冲区剩余空间校验的str_totext函数,并额外做返回值成功校验。而上述提到的str_totext函数与RETERR宏,在移植版本的masterdump.c中,RedHat开发者也都做了保留。但是,查看代码上下文发现,在RedHat开发者进行代码移植过程中,对官方代码进行了功能上的若干剪裁,包括一些细分数据类型记录的支持;而这里对缓冲区写入一字节,也许开发者完全没想到溢出的可能,所以自作主张地简化了代码调用过程。 问题思考 这个漏洞本身几乎没什么危害,但是背后足以引起思考。 没有人在“借”别人代码时能不出错 不同于之前章节提到的那种场景——将代码文件或片段复制到自己类似的代码上下文借用——backport作为一种官方且成熟的做法,借用的代码来源、粘贴到的代码上下文,是具有同源属性的,而且开发者一般是追求稳定性优先的社区开发人员,似乎质量应该有足够保障。但是这里的关键问题是:代码总要有一手、充分的语义理解,才能有可信的使用保障;因此,只要是处理他人的代码,因为不够理解而错误使用的风险,只可能减小,没办法消除。 如上分析,本次漏洞的产生看似只是做代码移植的开发者“自作主张”之下“改错了”。但是更广泛且可能的情况是,原始开发者在版本迭代中引入或更新大量基础数据结构、API的定义,并用在新的特性实现代码中;而后向移植开发人员仅需要最小规模的功能代码,所以会对增量代码进行一定规模的修改、剪裁、还原,以此适应旧版本基本代码。这些过程同样伴随着第三方开发人员不可避免的“望文生义”,以及随之而来的风险。后向移植操作也同样助长了软件碎片化过程,其中每一个碎片都存在这样的问题;每一个碎片在自身生命周期也将有持续性影响。 多级复制粘贴无异于雪上加霜 这里简单探讨的是企业通行的系统和基础软件建设实践。一些国内外厂商和社区发布的定制化Linux发行版,本身是有其它发行版,如CentOS特定版本渊源的,在基础软件上即便同其上游发行版最新版本间也存在断层滞后。RedHat相对于基础软件开发者之间已经隔了一层backport,而我们则人为制造了二级风险。 在很多基础而关键的软件上,企业系统基础设施的维护者出于与RedHat类似的初衷,往往会决定自行backport一份拷贝;通过早年心脏滴血事件的洗礼,即暴露出来OpenSSL一个例子。无论是需要RHEL还没来得及移植的新版本功能特性,还是出于对特殊使用上下文场景中更高执行效率的追求,企业都可能自行对RHEL上基础软件源码包进行修改定制重打包。这个过程除了将风险幂次放大外,也进一步加深了代码的不可解释性(包括基础软件开发人员流动性带来的不可解释)。 Ⅳ. 从武功到死穴:从systemd-journald信息泄露一窥API误用 1月10日凌晨两点,漏洞预警平台爬收取一封漏洞披露邮件。披露者是Qualys,那就铁定是重型发布了。最后看披露漏洞的目标,systemd?这就非常有意思了。 一些必要背景 systemd是什么,不好简单回答。Linux上面软件命名,习惯以某软件名后带个‘d’表示后台守护管理程序;所以systemd就可以说是整个系统的看守吧。而即便现在描述了systemd是什么,可能也很快会落伍,因为其初始及核心开发者Lennart Poettering(供职于Red Hat)描述它是“永无开发完结完整、始终跟进技术进展的、统一所有发行版无止境的差异”的一种底层软件。笼统讲有三个作用:中央化系统及设置管理;其它软件开发的基础框架;应用程序和系统内核之间的胶水。如今几乎所有Linux发行版已经默认提供systemd,包括RHEL/CentOS 7及后续版本。总之很基础、很底层、很重要就对了。systemd本体是个主要实现init系统的框架,但还有若干关键组件完成其它工作;这次被爆漏洞的是其journald组件,是负责系统事件日志记录的看守程序。 额外地还想简单提一句Qualys这个公司。该公司创立于1999年,官方介绍为信息安全与云安全解决方案企业,to B的安全业务非常全面,有些也是国内企业很少有布局的方面;例如上面提到的涉及碎片化和代码移植过程的历史漏洞移动,也在其漏洞管理解决方案中有所体现。但是我们对这家公司粗浅的了解来源于其安全研究团队近几年的发声,这两年间发布过的,包括有『stack clash』、『sudo get_tty_name提权』、『OpenSSH信息泄露与堆溢出』、『GHOST:glibc gethostbyname缓冲区溢出』等大新闻(仅截至2017年年中)。从中可见,这个研究团队专门啃硬骨头,而且还总能开拓出来新的啃食方式,往往爆出来一些别人没想到的新漏洞类型。从这个角度,再联想之前刷爆朋友圈的《安全研究者的自我修养》所倡导的“通过看历史漏洞、看别人的最新成果去举一反三”的理念,可见差距。 CVE-2018-16866漏洞详情 这次漏洞披露,打包了三个漏洞: ·16864和16865是内存破坏类型 ·16866是信息泄露 ·而16865和16866两个漏洞组和利用可以拿到root shell。 漏洞分析已经在披露中写的很详细了,这里不复述;而针对16866的漏洞成因来龙去脉,Qualys跟踪的结果留下了一点想象和反思空间,我们来看一下。 漏洞相关代码片段是这样的(漏洞修复前): 读者可以先肉眼过一遍这段代码有什么问题。实际上我一开始也没看出来,向下读才恍然大悟。 这段代码中,外部信息输入通过buf传入做记录处理。输入数据一般包含有空白字符间隔,需要分隔开逐个记录,有效的分隔符包括空格、制表符、回车、换行,代码中将其写入常量字符串;在逐字符扫描输入数据字符串时,将当前字符使用strchr在上述间隔符字符串中检索是否匹配,以此判断是否为间隔符;在240行,通过这样的判断,跳过记录单元字符串的头部连续空白字符。 但是问题在于,strchr这个极其基础的字符串处理函数,对于C字符串终止字符'\0'的处理上有个坑:'\0'也被认为是被检索字符串当中的一个有效字符。所以在240行,当当前扫描到的字符为字符串末尾的NULL时,strchr返回的是WHITESPACE常量字符串的终止位置而非NULL,这导致了越界。 看起来,这是一个典型的问题:API误用(API mis-use),只不过这个被误用的库函数有点太基础,让我忍不住想是不是还会有大量的类似漏洞……当然也反思我自己写的代码是不是也有同样情况,然而略一思考就释然了——我那么笨的代码都用for循环加if判断了:) 漏洞引入和消除历史 有意思的是,Qualys研究人员很贴心地替我做了一步漏洞成因溯源,这才是单独提这个漏洞的原因。漏洞的引入是在2015年的一个commit中: 在GitHub中,定位到上述2015年的commit信息,这里commit的备注信息为: journald: do not strip leading whitespace from messages. Keep leading whitespace for compatibility with older syslog implementations. Also useful when piping formatted output to the logger command. Keep removing trailing whitespace. OK,看起来是一个兼容性调整,对记录信息不再跳过开头所有连续空白字符,只不过用strchr的简洁写法比较突出开发者精炼的开发风格(并不),说得过去。 之后在2018年八月的一个当时尚未推正式版的另一次commit中被修复了,先是还原成了ec5ff4那次commit之前的写法,然后改成了加校验的方式: 虽然Qualys研究者认为上述的修改是“无心插柳”的改动,但是在GitHub可以看到,a6aadf这次commit是因为有外部用户反馈了输入数据为单个冒号情况下journald堆溢出崩溃的issue,才由开发者有目的性地修复的;而之后在859510这个commit再次改动回来,理由是待记录的消息都是使用单个空格作为间隔符的,而上一个commit粗暴地去掉了这种协议兼容性特性。 如果没有以上纠结的修改和改回历史,也许我会倾向于怀疑,在最开始漏洞引入的那个commit,既然改动代码没有新增功能特性、没有解决什么问题(毕竟其后三年,这个改动的代码也没有被反映issue),也并非出于代码规范等考虑,那么这么轻描淡写的一次提交,难免有人为蓄意引入漏洞的嫌疑。当然,看到几次修复的原因,这种可能性就不大了,虽然大家仍可以保留意见。但是抛开是否人为这个因素,单纯从代码的漏洞成因看,一个传统但躲不开的问题仍值得探讨:API误用。 API误用:程序员何苦为难程序员 如果之前的章节给读者留下了我反对代码模块化和复用的印象,那么这里需要正名一下,我们认可这是当下开发实践不可避免的趋势,也增进了社会开发速度。而API的设计决定了写代码和用代码的双方“舒适度”的问题,由此而来的API误用问题,也是一直被当做单纯的软件工程课题讨论。在此方面个人并没有什么研究,自然也没办法系统地给出分类和学术方案,只是谈一下自己的经验和想法。 一篇比较新的学术文章总结了API误用的研究,其中一个独立章节专门分析Java密码学组件API误用的实际,当中引述之前论文认为,密码学API是非常容易被误用的,比如对期望输入数据(数据类型,数据来源,编码形式)要求的混淆,API的必需调用次序和依赖缺失(比如缺少或冗余多次调用了初始化函数、主动资源回收函数)等。凑巧在此方面我有一点体会:曾经因为业务方需要,需要使用C++对一个Java的密码基础中间件做移植。Java对密码学组件支持,有原生的JDK模块和权威的BouncyCastle包可用;而C/C++只能使用第三方库,考虑到系统平台最大兼容和最小代码量,使用Linux平台默认自带的OpenSSL的密码套件。但在开发过程中感受到了OpenSSL满满的恶意:其中的API设计不可谓不反人类,很多参数没有明确的说明(比如同样是表示长度的函数参数,可能在不同地方分别以字节/比特/分组数为计数单位);函数的线程安全没有任何解释标注,需要自行试验;不清楚函数执行之后,是其自行做了资源释放还是需要有另外API做gc,不知道资源释放操作时是否规规矩矩地先擦除后释放……此类问题不一而足,导致经过了漫长的测试之后,这份中间件才提供出来供使用。而在业务场景中,还会存在比如其它语言调用的情形,这些又暴露出来OpenSSL API误用的一些完全无从参考的问题。这一切都成为了噩梦;当然这无法为我自己开解是个不称职开发的指责,但仅就OpenSSL而言其API设计之恶劣也是始终被人诟病的问题,也是之后其他替代者宣称改进的地方。 当然,问题是上下游都脱不了干系的。我们自己作为高速迭代中的开发人员,对于二方、三方提供的中间件、API,又有多少人能自信地说自己仔细、认真地阅读过开发指南和API、规范说明呢?做过通用产品技术运营的朋友可能很容易理解,自己产品的直接用户日常抛出不看文档的愚蠢问题带来的困扰。对于密码学套件,这个问题还好办一些,毕竟如果在没有背景知识的情况下对API望文生义地一通调用,绝大多数情况下都会以抛异常形式告终;但还是有很多情况,API误用埋下的是长期隐患。 不是所有API误用情形最终都有机会发展成为可利用的安全漏洞,但作为一个由人的因素引入的风险,这将长期存在并困扰软件供应链(虽然对安全研究者、黑客与白帽子是很欣慰的事情)。可惜,传统的白盒代码扫描能力,基于对代码语义的理解和构建,但是涉及到API则需要预先的抽象,这一点目前似乎仍然是需要人工干预的事情;或者轻量级一点的方案,可以case by case地分析,为所有可能被误用的API建模并单独扫描,这自然也有很强局限性。在一个很底层可信的开发者还对C标准库API存在误用的现实内,我们需要更多的思考才能说接下来的解法。 Ⅴ. 从规则到陷阱:NASA JIRA误配置致信息泄露血案 软件的定义包括了代码组成的程序,以及相关的配置、文档等。当我们说软件的漏洞、风险时,往往只聚焦在其中的代码中;关于软件供应链安全风险,我们的比赛、前面分析的例子也都聚焦在了代码的问题;但是真正的威胁都来源于不可思议之处,那么代码之外有没有可能存在来源于上游的威胁呢?这里就借助实例来探讨一下,在“配置”当中可能栽倒的坑。 引子:发不到500英里以外的邮件? 让我们先从一个轻松愉快的小例子引入。这个例子初见于Linux中国的一篇译文。 简单说,作者描述了这么一个让人啼笑皆非的问题:单位的邮件服务器发送邮件,发送目标距离本地500英里范围之外的一律失败,邮件就像悠悠球一样只能飞出一定距离。这个问题本身让描述者感到尴尬,就像一个技术人员被老板问到“为什么从家里笔记本上Ctrl-C后不能在公司台式机上Ctrl-V”一样。 经过令人窒息的分析操作后,笔者定位到了问题原因:笔者作为负责的系统管理员,把SunOS默认安装的Senmail从老旧的版本5升级到了成熟的版本8,且对应于新版本诸多的新特性进行了对应配置,写入配置文件sendmail.cf;但第三方服务顾问在对单位系统进行打补丁升级维护时,将系统软件“升级”到了系统提供的最新版本,因此将Sendmail实际回退到了版本5,却为了软件行为一致性,原样保留了高版本使用的配置文件。但Sendmail并没有在大版本间保证配置文件兼容性,这导致很多版本5所需的配置项不存在于保留下来的sendmail.cf文件中,程序按默认值0处理;最终引起问题的就是,邮件服务器与接收端通信的超时时间配置项,当取默认配置值0时,邮件服务器在1个单位时间(约3毫秒)内没有收到网络回包即认为超时,而这3毫秒仅够电信号打来回飞出500英里。 这个“故事”可能会给技术人员一点警醒,错误的配置会导致预期之外的软件行为,但是配置如何会引入软件供应链方向的安全风险呢?这就引出了下一个重磅实例。 JIRA配置错误致NASA敏感信息泄露案例 我们都听过一个事情,马云在带队考察美国公司期间问Google CEO Larry Page自视谁为竞争对手,Larry的回答是NASA,因为最优秀的工程师都被NASA的梦想吸引过去了。由此我们显然能窥见NASA的技术水位之高,这样的人才团队大概至少是不会犯什么低级错误的。 但也许需要重新定义“低级错误”……1月11日一篇技术文章披露,NASA某官网部署使用的缺陷跟踪管理系统JIRA存在错误的配置,可分别泄漏内部员工(JIRA系统用户)的全部用户名和邮件地址,以及内部项目和团队名称到公众,如下: 问题的原因解释起来也非常简单:JIRA系统的过滤器和配置面板中,对于数据可见性的配置选项分别选定为All users和Everyone时,系统管理人员想当然地认为这意味着将数据对所有“系统用户”开放查看,但是JIRA的这两个选项的真实效果逆天,是面向“任意人”开放,即不限于系统登录用户,而是任何查看页面的人员。看到这里,我不厚道地笑了……“All users”并不意味着“All ‘users’”,意不意外,惊不惊喜? 但是这种字面上把戏,为什么没有引起NASA工程师的注意呢,难道这样逆天的配置项没有在产品手册文档中加粗标红提示吗?本着为JIRA产品设计找回尊严的态度,我深入挖掘了一下官方说明,果然在Atlassian官方的一份confluence文档(看起来更像是一份增补的FAQ)中找到了相关说明: 所有未登录访客访问时,系统默认认定他们是匿名anonymous用户,所以各种权限配置中的all users或anyone显然应该将匿名用户包括在内。在7.2及之后版本中,则提供了“所有登录用户”的选项。 可以说是非常严谨且贴心了。比较讽刺的是,在我们的软件供应链安全大赛·C源代码赛季期间,我们设计圈定的恶意代码攻击目标还包括JIRA相关的敏感信息的窃取,但是却想不到有这么简单方便的方式,不动一行代码就可以从JIRA中偷走数据。 软件的使用,你“配”吗? 无论是开放的代码还是成型的产品,我们在使用外部软件的时候,都是处于软件供应链下游的消费者角色,为了要充分理解上游开发和产品的真实细节意图,需要我们付出多大的努力才够“资格”? 上一章节我们讨论过源码使用中必要细节信息缺失造成的“API误用”问题,而软件配置上的“误用”问题则复杂多样得多。从可控程度上讨论,至少有这几种因素定义了这个问题: ·软件用户对必要配置的现有文档缺少了解。这是最简单的场景,但又是完全不可避免的,这一点上我们所有有开发、产品或运营角色经验的应该都曾经体会过向不管不顾用户答疑的痛苦,而所有软件使用者也可以反省一下对所有软件的使用是否都以完整细致的文档阅读作为上手的准备工作,所以不必多说。 ·软件拥有者对配置条目缺少必要明确说明文档。就JIRA的例子而言,将NASA工程师归为上一条错误有些冤枉,而将JIRA归为这条更加合适。在边角但重要问题上的说明通过社区而非官方文档形式发布是一种不负责任的做法,但未引发安全事件的情况下还有多少这样的问题被默默隐藏呢?我们没办法要求在使用软件之前所有用户将软件相关所有文档、社区问答实现全部覆盖。这个问题范围内一个代表性例子是对配置项的默认值以及对应效果的说明缺失。 ·配置文件版本兼容性带来的误配置和安全问题。实际上,上面的SunOS Sendmail案例足以点出这个问题的存在性,但是在真实场景下,很可能不会以这么戏剧性形式出现。在企业的系统运维中,系统的版本迭代常见,但为软件行为一致性,配置的跨版本迁移是不可避免的操作;而且软件的更新迭代也不只会由系统更新推动,还有大量出于业务性能要求而主动进行的定制化升级,对于中小企业基础设施建设似乎是一个没怎么被提及过的问题。 ·配置项组合冲突问题。尽管对于单个配置项可能明确行为与影响,但是特定的配置项搭配可能造成不可预知的效果。这完全有可能是由于开发者与用户在信息不对等的情况下产生:开发者认为用户应该具有必需的背景知识,做了用户应当具备规避配置冲突能力的假设。一个例子是,对称密码算法在使用ECB、CBC分组工作模式时,从密码算法上要求输入数据长度必须是分组大小的整倍数,但如果用户搭配配置了秘钥对数据不做补齐(nopadding),则引入了非确定性行为:如果密码算法库对这种组合配置按某种默认补齐方式操作数据则会引起歧义,但如果在算法库代码层面对这种组合抛出错误则直接影响业务。 ·程序对配置项处理过程的潜在暗箱操作。这区别于简单的未文档化配置项行为,仅特指可能存在的蓄意、恶意行为。从某种意义上,上述“All users”也可以认为是这样的一种陷阱,通过浅层次暗示,引导用户做出错误且可能引起问题的配置。另一种情况是特定配置组合情况下触发恶意代码的行为,这种触发条件将使恶意代码具有规避检测的能力,且在用户基数上具有一定概率的用户命中率。当然这种情况由官方开发者直接引入的可能性很低,但是在众包开发的情况下如果存在,那么扫描方案是很难检测的。 Ⅵ. 从逆流到暗流:恶意代码溯源后的挑战 如果说前面所说的种种威胁都是面向关键目标和核心系统应该思考的问题,那么最后要抛出一个会把所有人拉进赛场的理由。除了前面所有那些在软件供应链下游被动污染受害的情况,还有一种情形:你有迹可循的代码,也许在不经意间会“反哺”到黑色产业链甚至特殊武器中;而现在研究用于对程序进行分析和溯源的技术,则会让你陷入百口莫辩的境地。 案例:黑产代码模块溯源疑云 1月29日,猎豹安全团队发布技术分析通报文章《电信、百度客户端源码疑遭泄漏,驱魔家族窃取隐私再起波澜》,矛头直指黑产上游的恶意信息窃取代码模块,认定其代码与两方产品存在微妙的关联:中国电信旗下“桌面3D动态天气”等多款软件,以及百度旗下“百度杀毒”等软件(已不可访问)。 文章中举证有三个关键点。 首先最直观的,是三者使用了相同的特征字符串、私有文件路径、自定义内部数据字段格式; 其次,在关键代码位置,三者在二进制程序汇编代码层面具有高度相似性; 最终,在一定范围的非通用程序逻辑上,三者在经过反汇编后的代码语义上显示出明显的雷同,并提供了如下两图佐证(图片来源): 文章指出的涉事相关软件已经下线,对于上述样本文件的相似度试验暂不做复现,且无法求证存在相似、疑似同源的代码在三者中占比数据。对于上述指出的代码雷同现象,猎豹安全团队认为: 我们怀疑该病毒模块的作者通过某种渠道(比如“曾经就职”),掌握有中国电信旗下部分客户端/服务端源码,并加以改造用于制作窃取用户隐私的病毒,另外在该病毒模块的代码中,我们还发现“百度”旗下部分客户端的基础调试日志函数库代码痕迹,整个“驱魔”病毒家族疑点重重,其制作传播背景愈发扑朔迷离。 这样的推断,固然有过于直接的依据(例如三款代码中均使用含有“baidu”字样的特征注册表项);但更进一步地,需要注意到,三个样本在所指出的代码位置,具有直观可见的二进制汇编代码结构的相同,考虑到如果仅仅是恶意代码开发者先逆向另外两份代码后借鉴了代码逻辑,那么在面临反编译、代码上下文适配重构、跨编译器和选项的编译结果差异等诸多不确定环节,仍能保持二进制代码的雷同,似乎确实是只有从根本上的源代码泄漏(抄袭)且保持相同的开发编译环境才能成立。 但是我们却又无法做出更明确的推断。这一方面当然是出于严谨避免过度解读;而从另一方面考虑,黑产代码的一个关键出发点就是“隐藏自己”,而这里居然如此堂而皇之地照搬了代码,不但没有进行任何代码混淆、变形,甚至没有抹除疑似来源的关键字符串,如果将黑产视为智商在线的对手,那这里背后是否有其它考量,就值得琢磨了。 代码的比对、分析、溯源技术水准 上文中的安全团队基于大量样本和粗粒度比对方法,给出了一个初步的判断和疑点。那么是否有可能获得更确凿的分析结果,来证实或证伪同源猜想呢? 无论是源代码还是二进制,代码比对技术作为一种基础手段,在软件供应链安全分析上都注定仍然有效。在我们的软件供应链安全大赛期间,针对PE二进制程序类型的题目,参赛队伍就纷纷采用了相关技术手段用于目标分析,包括:同源性分析,用于判定与目标软件相似度最高的同软件官方版本;细粒度的差异分析,用于尝试在忽略编译差异和特意引入的混淆之外,定位特意引入的恶意代码位置。当然,作为比赛中针对性的应对方案,受目标和环境引导约束,这些方法证明了可行性,却难以保证集成有最新技术方案。那么做一下预言,在不计入情报辅助条件下,下一代的代码比对将能够到达什么水准? 这里结合近一年和今年内,已发表和未发表的学术领域顶级会议的相关文章来简单展望: ·针对海量甚至全量已知源码,将可以实现准确精细化的“作者归属”判定。在ACM CCS‘18会议上曾发表的一篇文章《Large-Scale and Language-Oblivious Code Authorship Identification》,描述了使用RNN进行大规模代码识别的方案,在圈定目标开发者,并预先提供每个开发者的5-7份已知的代码文件后,该技术方案可以很有效地识别大规模匿名代码仓库中隶属于每个开发者的代码:针对1600个Google Code Jam开发者8年间的所有代码可以实现96%的成功识别率,而针对745个C代码开发者于1987年之后在GitHub上面的全部公开代码仓库,识别率也高达94.38%。这样的结果在当下的场景中,已经足以实现对特定人的代码识别和跟踪(例如,考虑到特定开发人员可能由于编码习惯和规范意识,在时间和项目跨度上犯同样的错误);可以预见,在该技术方向上,完全可以期望摆脱特定已知目标人的现有数据集学习的过程,并实现更细粒度的归属分析,例如代码段、代码行、提交历史。 ·针对二进制代码,更准确、更大规模、更快速的代码主程序分析和同源性匹配。近年来作为一项程序分析基础技术研究,二进制代码相似性分析又重新获得了学术界和工业界的关注。在2018年和2019(已录用)的安全领域四大顶级会议上,每次都会有该方向最新成果的展示,如S&P‘2019上录用的《Asm2Vec: Boosting Static Representation Robustness for Binary Clone Search against Code Obfuscation and Compiler Optimization》,实现无先验知识的条件下的最优汇编代码级别克隆检测,针对漏洞库的漏洞代码检测可实现0误报、100%召回。而2018年北京HITB会议上,Google Project Zero成员、二进制比对工具BinDiff原始作者Thomas Dullien,探讨了他借用改造Google自家SimHash算法思想,用于针对二进制代码控制流图做相似性检测的尝试和阶段结果;这种引入规模数据处理的思路,也可期望能够在目前其他技术方案大多精细化而低效的情况下,为高效、快速、大规模甚至全量代码克隆检测勾出未来方案。 ·代码比对方案对编辑、优化、变形、混淆的对抗。近年所有技术方案都以对代码“变种”的检测有效性作为关键衡量标准,并一定程度上予以保证。上文CCS‘18论文工作,针对典型源代码混淆(如Tigress)处理后的代码,大规模数据集上可有93.42%的准确识别率;S&P‘19论文针对跨编译器和编译选项、业界常用的OLLVM编译时混淆方案进行试验,在全部可用的混淆方案保护之下的代码仍然可以完成81%以上的克隆检测。值得注意的是以上方案都并非针对特定混淆方案单独优化的,方法具有通用价值;而除此以外还有很多针对性的的反混淆研究成果可用;因此,可以认为在采用常规商用代码混淆方案下,即便存在隐藏内部业务逻辑不被逆向的能力,但仍然可以被有效定位代码复用和开发者自然人。 代码溯源技术面前的“挑战” 作为软件供应链安全的独立分析方,健壮的代码比对技术是决定性的基石;而当脑洞大开,考虑到行业的发展,也许以下两种假设的情景,将把每一个“正当”的产品、开发者置于尴尬的境地。 代码仿制 在本章节引述的“驱魔家族”代码疑云案例中,黑产方面通过某种方式获得了正常代码中,功能逻辑可以被自身复用的片段,并以某种方法将其在保持原样的情况下拼接形成了恶意程序。即便在此例中并非如此,但这却暴露了隐忧:将来是不是有这种可能,我的正常代码被泄漏或逆向后出现在恶意软件中,被溯源后扣上黑锅? 这种担忧可能以多种渠道和形式成为现实。 从上游看,内部源码被人为泄漏是最简单的形式(实际上,考虑到代码的完整生命周期似乎并没有作为企业核心数据资产得到保护,目前实质上有没有这样的代码在野泄漏还是个未知数),而通过程序逆向还原代码逻辑也在一定程度上可获取原始代码关键特征。 从下游看,则可能有多种方式将恶意代码伪造得像正常代码并实现“碰瓷”。最简单地,可以大量复用关键代码特征(如字符串,自定义数据结构,关键分支条件,数据记录和交换私有格式等)。考虑到在进行溯源时,分析者实际上不需要100%的匹配度才会怀疑,因此仅仅是仿造原始程序对于第三方公开库代码的特殊定制改动,也足以将公众的疑点转移。而近年来类似自动补丁代码搜索生成的方案也可能被用来在一份最终代码中包含有二方甚至多方原始代码的特征和片段。 基于开发者溯源的定点渗透 既然在未来可能存在准确将代码与自然人对应的技术,那么这种技术也完全可能被黑色产业利用。可能的忧患包括强针对性的社会工程,结合特定开发者历史代码缺陷的漏洞挖掘利用,联动第三方泄漏人员信息的深层渗透,等等。这方面暂不做联想展开。 〇. 没有总结 作为一场旨在定义“软件供应链安全”威胁的宣言,阿里安全“功守道”大赛将在后续给出详细的分解和总结,其意义价值也许会在一段时间之后才能被挖掘。 但是威胁的现状不容乐观,威胁的发展不会静待;这一篇随笔仅仅挑选六个侧面做摘录分析,可即将到来的趋势一定只会进入更加发散的境地,因此这里,没有总结。 本篇文章为转载内容。原文链接:https://blog.csdn.net/systemino/article/details/90114743。 该文由互联网用户投稿提供,文中观点代表作者本人意见,并不代表本站的立场。 作为信息平台,本站仅提供文章转载服务,并不拥有其所有权,也不对文章内容的真实性、准确性和合法性承担责任。 如发现本文存在侵权、违法、违规或事实不符的情况,请及时联系我们,我们将第一时间进行核实并删除相应内容。
2023-02-05 13:33:43
300
转载
转载文章
...内容简介 《游戏引擎架构》同时涵盖游戏引擎软件开发的理论及实践,并对多方面的题目进行探讨。本书讨论到的概念及技巧实际应用于现实中的游戏工作室,如艺电及顽皮狗。虽然书中采用的例子通常依据一些专门的技术,但是讨论范围远超于某个引擎或API。文中的参考及引用也非常有用,可让读者继续深入游戏开发过程的任何特定方向。 《游戏引擎架构》为一个大学程度的游戏编程课程而编写,但也适合软件工程师、业余爱好者、自学游戏程序员,以及游戏产业的从业人员。通过阅读《游戏引擎架构》,资历较浅的游戏工程师可以巩固他们所学的游戏技术及引擎架构的知识,专注某一领域的资深程序员也能从本书更为全面的介绍中获益。 內容包括: 游戏开发中的大规模C++软件架构 游戏编程所需的数学 供调试、源代码控制及性能剖析的游戏开发工具 引擎基础系统、渲染、碰撞、物理、角色动画、游戏世界对象模型等引擎子系统 多平台游戏引擎 多处理器环境下的游戏编程 工作管道及游戏资产数据库 作者/译者简介 作者介绍:Jason Gregory在1994年开始任职专业软件工程师,自1999年3月开始在游戏产业中任职软件工程师。在圣迭哥Midway Home Entertainment公司开始游戏编程的他,为《疯狂飞行员(Freaky Flyers)》及《Crank the Weasel》开发PlayStation 2/Xbox上的动画系统。在2003年,他转到洛杉矶艺电,为《荣誉勋章:血战太平洋(Medal of Honor: Pacific Assault)》开发游戏引擎及游戏性技术,并在《荣誉勋章:空降神兵(Medal of Honor: Airborne)》中担任首席工程师。他现时是顽皮狗公司的通才程序员,为《神秘海域:德雷克船长的宝藏(Uncharted: Drake's Fortune)》及《神秘海域:纵横四海(Uncharted: Among Thieves)》开发引擎及游戏性软件。他也在南加州大学教授游戏技术的课程。 译者简介:叶劲峰(Milo Yip)从小自习编程,并爱好计算机图形学。上中学时兼职开发策略RPG《王子传奇》,该游戏在1995年于台湾发行。其后他获取了香港大学认知科学学士、香港中文大学系统工程及工程管理哲学硕士。毕业后在香港理工大学设计学院从事游戏引擎及相关技术的研发,职至项目主任。除发表学术文章外,也曾合著《DirectX9游戏编程实务》。2008年往上海育碧担任引擎工程师开发《美食从天而降(Cloudy with a Chance of Meatballs)》Xbox360/PS3/Wii/PC,2009年起于麻辣马开发《爱丽丝:疯狂回归(Alice: Madness Returns)》Xbox360/PS3/PC,2011年加入腾讯互动娱乐引擎技术中心担任专家工程师,所研发的技术已用于《斗战神》、《天涯明月刀》、《众神争霸》等项目中。 推荐序1 最初拿到《Game Engine Architecture》一书的英文版,是编辑侠少邮寄给我的打印版。他建议我接下翻译此书的合同。当时我正在杭州带领一个团队开发3D游戏引擎,我和我的同事都对这本书的内容颇有兴趣,两大本打印的英文书立刻在同事间传开。可惜那段时间个人精力顾及不来,把近千页的英文读物精读而后翻译成中文对个人的业余时间是个极大的挑战,不能担此翻译任务颇为遗憾。 不久以后听说Milo Yip(叶劲峰)已开始着手翻译,甚为欣喜。翻译此巨著,他一定是比我更合适的人选。我和Milo虽未曾蒙面,但神交已久。在网络上读过一些他的成长经历,和我颇为相似,心有戚戚。他对游戏3D实时渲染技术研究精深为我所不及,我们曾通过Google Talk讨论过许多技术问题,他都有独到的见解。翻译工作开始后,Milo是香港人,英文技术术语在香港的中文译法和大陆的有许多不同。但此书由大陆出版社出版,考虑到面对的读者主要是大陆程序员,Milo希望能更符合大陆程序员的用词习惯,所以在翻译一开始就通过Google Docs创建了协作页面,邀请大家共同探讨书中技术名词的中译名。从中我们可以一窥他作为译者的慎重。 三年之后,有幸在出版之前就拿到了完整的译本。这是一本用LaTeX精心排版的800页的电子书,我只花了一周时间,几乎是一口气读完。流畅的阅读享受,绝对不仅仅是因为原著精彩的内容,精美的版面和翔实的译注也加了不少分。 在阅读本书的过程中,我不只一次地获得共鸣。例如在第5章的内存管理系统的介绍中,作者介绍的几种游戏特有的内存管理方法我都曾在项目中用过,而这是第一次有书籍专门将这些方法详尽记录;又如第11章动画系统的介绍,我们也同样在3D引擎开发过程中改进原有动画片段混合方法的经历。虽然书中介绍的每个技术点,都可能可以在某篇论文,某本其他的书的章节,某篇网络blog上见过,但之前却无一本书可以把这些东西放在一起相互参照。对于从事游戏引擎开发的程序员来说,了解各种引擎在处理每个具体问题时的方案是相当重要的。而每种方案又各有利弊,即使不做引擎开发工作而是在某一特定游戏引擎上做游戏开发,从中也可以理解引擎的局限性以及可能的改进方法。尤其是第14章介绍的对游戏性相关系统的设计,各个开发人员几乎都是凭经验设计,很少见有书籍对这些做总结。对于基于渲染引擎做开发的游戏程序员,这是必须面对的工作,这一章会有很大的借鉴意义。 本书作者是业内资深的游戏引擎开发人,他所参于的《神秘海域》和《最后生还者》都是我的个人最爱。在玩游戏的过程中,作为游戏程序员的天性,自然会不断地猜想各个技术点是如何实现的,背后需要怎样的工具支持。能在书中一一得到印证是件特别开心的事情。作者反复强调代码实践的重要性,在书中遍布着C++代码。我不认为这些代码有直接取来使用的价值,但它们极大地帮助了读者理解书中的技术点。书中列出的顽皮狗工作室用lisp方言作为游戏配置脚本的范例也给我很大的启发,有了这些具体的代码示例以及作者本身的一线工程师背景,也让我确信书中那些关于主机游戏开发相关等,我所没有接触过的内容都也绝非泛泛而谈。 国内的游戏开发社区的壮大,主要是随最近十年的MMO风潮而生。而就在大型网络游戏在中国有些畸形发展,让这类游戏偏离电子游戏游戏性的趋势时,我们有幸迎来了为移动设备开发游戏的大潮。游戏开发的重心重新回到游戏性本身。我们更需要去借鉴单机游戏是如何为玩家带来更纯粹的游戏体验,我相信书中记录的各种技术点会变的更有帮助。 资深游戏开发及创业者 云风 @简悦云风 推荐序2 在我认识的许多游戏业开发同仁中,只有少数香港同胞,Milo Yip(叶劲峰)却正是这样一位给我印象非常深刻的优秀香港游戏开发者。我俩认识,是在Milo加入腾讯互动娱乐研发部引擎技术中心后,说来到现在也只是两年多时间。其间,他为人的谦逊务实,对待技术问题的严谨求真态度,对算法设计和性能优化的娴熟技术,都为人所称道。Milo一丝不苟的工作风格,甚至表现在对待技术文档排版这类事情上(Milo常执著地用LaTeX将技术文档排到完美),我想这一定是他在香港读大学、硕士及在香港理工大学的多媒体创新中心从事研究员,一贯沿袭至今的好作风。 我很高兴腾讯游戏有实力吸引到这样优秀的技术专家;即使在其已从上海迁回香港家中,依然选择到深圳腾讯互动娱乐总部工作。叶兄从此工作日每天早晚过关,来往香港和深圳两地,虽有舟车劳顿,但是兼顾了对家庭的照顾和在游戏引擎方面的专业研究,希望这样的状况是令他满意的。 认识叶兄当时,我便知道他在进行Jason Gregory所著《游戏引擎架构》一书的中译工作。因为自己从前也有业余翻译游戏开发有关书籍的经历,所以我能理解其中的辛苦和责任重大,对叶兄也更多一分钦佩。我以为,本书以及本书的中文读者最大的幸运便是,遇到叶兄这位对游戏有着如同对家对国般强烈责任感,犹如“游戏科学工作者”般的专业译者! 现在(2013年年末)无疑是游戏史上对独立游戏制作者最友好的年代。开发设备方便获得(相对过往仅由主机厂商授权才能获得专利开发设备,现在有一台智能手机和一台个人电脑就可以开发)、技术工具友好、调试过程简单方便,且互联网上有丰富的例程和开源代码参考,也有网上社区便于交流。很多爱好者能够很快地制作出可运行的游戏原型,其中一些也能发布到应用商店。 但是不全面掌握各方面知识,尤其是游戏引擎架构知识,往往只能停留在勉强修改、凑合重用别人提供的资源的应用程度上,难以做极限的性能改进,更妄谈革命式的架构创新。这样的程度是很难在成千上万的游戏中脱颖而出的。我们所认可的真正的游戏大作,必定是在某方面大幅超越用户期待的产品。为了打造这样的产品,游戏内容创作者(策划、美术等)需要“戴着镣铐跳舞”(在当前的机能下争取更多的创作自由度),而引擎架构合理的游戏可以经得起──也值得进行──反复优化,最终可以提供更多的自由度,这是大作出现的技术前提。 书的作者、译者、出版社的编者,加上读者,大家是因书而结缘的有缘人。因叶兄这本《游戏引擎架构》译著而在线上线下相识的读者们,你们是不是因“了解游戏引擎架构,从而制作/优化好游戏”这样的理想而结了缘呢? 亲爱的读者,愿你的游戏有一天因谜题巧妙绝伦、趣味超凡、虚拟世界气势磅礴、视觉效果逼真精美等专业因素取得业界褒奖,并得到玩家真诚的赞美。希望届时曾读叶兄这本《游戏引擎架构》译作的你,也可以回馈社会,回馈游戏开发的学习社区,帮助新人。希望你也可以建立微信公众号、博客等,或翻译游戏开发书籍,造福外语不好的读者,所以如果你的外语(英语、日语、韩语之于游戏行业比较重要)水平仍需精进,现在也可以同步加油了! 腾讯《天天爱消除》游戏团队Leader 沙鹰 @也是沙鹰 译序 数千年以来,艺术家们通过文学、绘画、雕塑、建筑、音乐、舞蹈、戏剧等传统艺术形式充实人类的精神层面。自20世纪中叶,计算机的普及派生出另一种艺术形式──电子游戏。游戏结合了上述传统艺术以及近代科技派生的其他艺术(如摄影、电影、动画),并且完全脱离了艺术欣赏这种单向传递的方式──游戏必然是互动的,“玩家”并不是“读者”、“观众”或“听众”,而是进入游戏世界、感知并对世界做出反应的参与者。 基于游戏的互动本质,游戏的制作通常比其他大众艺术复杂。商业游戏的制作通常需要各种人才的参与,而他们则需要依赖各种工具及科技。游戏引擎便是专门为游戏而设计的工具及科技集成。之所以称为引擎,如同交通工具中的引擎,提供了最核心的技术部分。因为复杂,研发成本高,人们不希望制作每款游戏(或车款)时都重新设计引擎,重用性是游戏引擎的一个重要设计目标。 然而,各游戏本身的性质以及平台的差异,使研发完全通用的游戏引擎变得极困难,甚至不可能。市面上出售的游戏引擎,有一些虽然已经达到很高的技术水平,但在商业应用中,很多时候还是需要因应个别游戏项目对引擎改造、整合、扩展及优化。因此,即使能使用市面上最好的商用引擎或自研引擎,我们仍需要理解当中的架构、各种机制和技术,并且分析及解决在制作中遇到的问题。这些也是译者曾任于上海两家工作室时的主要工作范畴。 选择翻译此著作,主要原因是在阅读中得到共鸣,并且能知悉一些知名游戏作品实际上所采用的方案。有感坊间大部分游戏开发书籍并不是由业内人士执笔,内容只足够应付一些最简单的游戏开发,欠缺宏观比较各种方案,技术与当今实际情况也有很大差距。而一些Gems类丛书虽然偶有好文章,但受形式所限欠缺系统性、全面性。难得本书原作者身为世界一流游戏工作室的资深游戏开发者(注1),在繁重的游戏开发工作外,还在大学教授游戏开发课程以至编写本著作。此外,从与内地同事的交流中,了解到许多从业者不愿意阅读外文书籍。为了普及知识及反馈业界社会,希望能尽绵力。 或许有些人以为本著作是针对单机/游戏机游戏的,并不适合国内以网游为主的环境。但译者认为这是一种误解,许多游戏本身所涉及的技术是具通用性的。例如游戏性相关的游戏性系统、场景管理、人工智能、物理模拟等部分,许多时候也会同时用于网游的前台和后台。现时,一些动作为主、非MMO的国内端游甚至会直接在后台运行传统意义上的游戏引擎。至于前台相关的技术,单机和端游的区别更少。此外,随着近年移动终端的兴起,其硬件性能已超越传统掌上游戏机,开发手游所需的技术与传统掌上游戏机并无太大差异。还可预料,现时单机/游戏机的一些较高级的架构及技术,将在不远的未来着陆移动终端平台。 译者认为,本书涵括游戏开发技术的方方面面,同时适合入门及经验丰富的游戏程序员。书名中的架构二字,并不单是给出一个系统结构图,而是描述每个子系统的需求、相关技术及与其他子系统的关系。对译者本人而言,本书的第11章(动画系统)及第14章(运行时游戏性基础系统)是本书特別精彩之处,含有许多少见于其他书籍的内容。而第10章(渲染引擎)由于是游戏引擎中的一个极大的部分,有限的篇幅可能未能覆盖广度及深度,推荐读者参考[1](注2),人工智能方面也需参考其他专著。 本译作采用LaTeX排版(注3),以Inkscape编译矢量图片。为了令阅读更流畅,内文中的网址都统一改以脚注标示。另外,由于现时游戏开发相关的文献以英文为主,而且游戏开发涉及的知识面很广,本译作尽量以括号形式保留英文术语。为了方便读者查找内容,在附录中增设中英文双向索引(索引条目与原著的不同)。 本人在香港成长学习及工作,至2008年才赴内地游戏工作室工作,不黯内地的中文写作及用字习惯,翻译中曾遇到不少困难。有幸得到出版社人员以及良师益友的帮助,才能完成本译作。特别感谢周筠老师支持本作的提案,并耐心地给予协助及鼓励。编辑张春雨老师和卢鸫翔老师,以及好友余晟给予了大量翻译上的知识及指导。也感谢游戏业界专家云风、大宝和Dave给予了许多宝贵意见。此书的翻译及排版工作比预期更花时间,感谢妻子及儿女们的体谅。此次翻译工作历时三年半,因工作及家庭事宜导致严重延误,唯有在翻译及排版工作上更尽心尽力,希望求得等待此译作的读者们谅解。无论是批评或建议,诚希阁下通过电邮miloyip@gmail.com、新浪微博、豆瓣等渠道不吝赐教。 叶劲峰(Milo Yip) 2013年10月 原作者是顽皮狗(Naughty Dog)《神秘海域(Uncharted)》系列的通才程序员、《最后生还者(The Last of Us)》的首席程序员,之前还曾在EA和Midway工作。 中括号表示引用附录中的参考文献。一些参考条目加入了其中译本的信息。 具体是使用CTEX套装,它是在MiKTeX的基础上增加中文的支持。 前言 最早的电子游戏完全由硬件构成,但微处理器(microprocessor)的高速发展完全改变了游戏的面貌。现在的游戏是在多用途的PC和专门的电子游戏主机(video game console)上玩的,凭借软件带来绝妙的游戏体验。从最初的游戏诞生至今已有半个世纪,但很多人仍然认为游戏是一个未成熟的产业。即使游戏可能是个年轻的产业,若仔细观察,也会发现它正在高速发展。 现时游戏已成为一个上百亿美元的产业,覆盖不同年龄、性别的广泛受众。 千变万化的游戏,可以分为从纸牌游戏到大型多人在线游戏(massively multiplayer online game,MMOG)等多个种类(category)和“类型(genre)”(注1),也可以运行在任何装有微芯片(microchip)的设备上 。你现在可以在PC、手机及多种特别为游戏而设计的手持/电视游戏主机上玩游戏。家用电视游戏通常代表最尖端的游戏科技,又由于它们是周期性地推出新版本,因此有游戏机“世代”(generation)的说法。最新一代(注2)的游戏机包括微软的Xbox 360和索尼的PlayStation 3,但一定不可忽视长盛不衰的PC,以及最近非常流行的任天堂Wii。 最近,剧增的下载式休闲游戏,使这个多样化的商业游戏世界变得更复杂。虽然如此,大型游戏仍然是一门大生意。今天的游戏平台非常复杂,有难以置信的运算能力,这使软件的复杂度得以进一步提升。所有这些先进的软件都需要由人创造出来,这导致团队人数增加,开发成本上涨。随着产业变得成熟,开发团队要寻求更好、更高效的方式去制作产品,可复用软件(reusable software)和中间件(middleware)便应运而生,以补偿软件复杂度的提升。 由于有这么多风格迥异的游戏及多种游戏平台,因此不可能存在单一理想的软件方案。然而,业界已经发展出一些模式 ,也有大量的潜在方案可供选择。现今的问题是如何找到一个合适的方案去迎合某个项目的需要。再进一步,开发团队必须考虑项目的方方面面,以及如何把各方面集成。对于一个崭新的游戏设计,鲜有可能找到一个完美搭配游戏设计各方面的软件包。 现时业界内的老手,入行时都是“开荒牛”。我们这代人很少是计算机科学专业出身(Matt的专业是航空工程、Jason的专业是系统设计工程),但现时很多学院已设有游戏开发的课程和学位。时至今日,为了获取有用的游戏开发信息,学生和开发者必须找到好的途径。对于高端的图形技术,从研究到实践都有大量高质量的信息。可是,这些信息经常不能直接应用到游戏的生产环境,或者没有一个生产级质量的实现。对于图形以外的游戏开发技术,市面上有一些所谓的入门书籍,没提及参考文献就描述很多内容细节,像自己发明的一样。这种做法根本没有用处,甚至经常带有不准确的内容。另一方面,市场上有一些高端的专门领域书籍,例如物理、碰撞、人工智能等。可是,这类书或者啰嗦到让你难以忍受,或者高深到让部分读者无法理解,又或者内容过于零散而难于融会贯通。有一些甚至会直接和某项技术挂钩,软硬件一旦改动,其内容就会迅速过时。 此外,互联网也是收集相关知识的绝佳工具。可是,除非你确实知道要找些什么,否则断链、不准确的资料、质量差的内容也会成为学习障碍。 好在,我们有Jason Gregory,他是一位拥有在顽皮狗(Naughty Dog)工作经验的业界老手,而顽皮狗是全球高度瞩目的游戏工作室之一。Jason在南加州大学教授游戏编程课程时,找不到概括游戏架构的教科书。值得庆幸的是,他承担了这个任务,填补了这个空白。 Jason把应用到实际发行游戏的生产级别知识,以及整个游戏开发的大局编集于本书。他凭经验,不仅融汇了游戏开发的概念和技巧,还用实际的代码示例及实现例子去说明怎样贯通知识来制作游戏。本书的引用及参考文献可以让读者更深入探索游戏开发过程的各方面。虽然例子经常是基于某些技术的,但是概念和技巧是用来实际创作游戏的,它们可以超越个别引擎或API的束缚。 本书是一本我们入行做游戏时想要的书。我们认为本书能让入门者增长知识,也能为有经验者开拓更大的视野。 Jeff Lander(注3) Matthew Whiting(注4) 译注:Genre一词在文学中为体裁。电影和游戏里通常译作类型。不同的游戏类型可见1.2节。 译注:按一般说法,2005年至今属于第7个游戏机世代。这3款游戏机的发行年份为Xbox 360(2005)、PlayStation 3(2006)、Wii(2006)。有关游戏机世代可参考维基百科。 译注:Jeff Lander现时为Darwin 3D公司的首席技术总监、Game Tech公司创始人,曾为艺电首席程序员、Luxoflux公司游戏性及动画技术程序员。 译注:Matthew Whiting现时为Wholesale Algorithms公司程序员,曾为Luxoflux公司首席软件工程师、Insomniac Games公司程序员。 序言 欢迎来到《游戏引擎架构》世界。本书旨在全面探讨典型商业游戏引擎的主要组件。游戏编程是一个庞大的主题,有许多内容需要讨论。不过相信你会发现,我们讨论的深度将足以使你充分理解本书所涵盖的工程理论及常用实践的方方面面。话虽如此,令人着迷的漫长游戏编程之旅其实才刚刚启程。与此相关的每项技术都包含丰富内容,本书将为你打下基础,并引领你进入更广阔的学习空间。 本书焦点在于游戏引擎的技术及架构。我们会探讨商业游戏引擎中,各个子系统的相关理论,以及实现这些理论所需要的典型数据结构、算法和软件接口。游戏引擎与游戏的界限颇为模糊。我们将把注意力集中在引擎本身,包括多个低阶基础系统(low-level foundation system)、渲染引擎(rendering engine)、碰撞系统(collision system)、物理模拟(physics simulation)、人物动画(character animation),及一个我称为游戏性基础层(gameplay foundation layer)的深入讨论。此层包括游戏对象模型(game object model)、世界编辑器(world editor)、事件系统(event system)及脚本系统(scripting system)。我们也将会接触游戏性编程(gameplay programming)的多个方面,包括玩家机制(player mechanics)、摄像机(camera)及人工智能(artificial intelligence,AI)。然而,这类讨论会被限制在游戏性系统和引擎接口范围。 本书可以作为大学中等级游戏程序设计中两到三门课程的教材。当然,本书也适合软件工程师、业余爱好者、自学的游戏程序员,以及游戏行业从业人员。通过阅读本书,资历较浅的游戏程序员可以巩固他们所学的游戏数学、引擎架构及游戏科技方面的知识。专注某一领域的资深程序员也能从本书更为全面的介绍中获益。 为了更好地学习本书内容,你需要掌握基本的面向对象编程概念并至少拥有一些C++编程经验。尽管游戏行业已经开始尝试使用一些新的、令人兴奋的编程语言,然而工业级的3D游戏引擎仍然是用C或C++编写的,任何认真的游戏程序员都应该掌握C++。我们将在第3章重温一些面向对象编程的基本原则,毫无疑问,你还会从本书学到一些C++的小技巧,不过C++的基础最好还是通过阅读[39]、[31]及[32]来获得。如果你对C++已经有点生疏,建议你在阅读本书的同时,最好能重温这几本或者类似书籍。如果你完全没有C++经验,在看本书之前,可以考虑先阅读[39]的前几章,或者尝试学习一些C++的在线教程。 学习编程技能最好的方法就是写代码。在阅读本书时,强烈建议你选择一些特别感兴趣的主题付诸实践。举例来说,如果你觉得人物动画很有趣,那么可以首先安装OGRE,并测试一下它的蒙皮动画示范。接着还可以尝试用OGRE实现本书谈及的一些动画混合技巧。下一步你可能会打算用游戏手柄控制人物在平面上行走。等你能玩转一些简单的东西了,就应该以此为基础,继续前进!之后可以转移到另一个游戏技术范畴,周而复始。这些项目是什么并不重要,重要的是你在实践游戏编程的艺术,而不是纸上谈兵。 游戏科技是一个活生生、会呼吸的家伙 ,永远不可能将之束缚于书本之上 。因此,附加的资源、勘误、更新、示例代码、项目构思等已经发到本书的网站。 目录 推荐序1 iii推荐序2 v译序 vii序言 xvii前言 xix致谢 xxi第一部分 基础 1第1章 导论 31.1 典型游戏团队的结构 41.2 游戏是什么 71.3 游戏引擎是什么 101.4 不同游戏类型中的引擎差异 111.5 游戏引擎概观 221.6 运行时引擎架构 271.7 工具及资产管道 46第2章 专业工具 532.1 版本控制 532.2 微软Visual Studio 612.3 剖析工具 782.4 内存泄漏和损坏检测 792.5 其他工具 80第3章 游戏软件工程基础 833.1 重温C++及最佳实践 833.2 C/C++的数据、代码及内存 903.3 捕捉及处理错误 118第4章 游戏所需的三维数学 1254.1 在二维中解决三维问题 1254.2 点和矢量 1254.3 矩阵 1394.4 四元数 1564.5 比较各种旋转表达方式 1644.6 其他数学对象 1684.7 硬件加速的SIMD运算 1734.8 产生随机数 180第二部分 低阶引擎系统 183第5章 游戏支持系统 1855.1 子系统的启动和终止 1855.2 内存管理 1935.3 容器 2085.4 字符串 2255.5 引擎配置 234第6章 资源及文件系统 2416.1 文件系统 2416.2 资源管理器 251第7章 游戏循环及实时模拟 2777.1 渲染循环 2777.2 游戏循环 2787.3 游戏循环的架构风格 2807.4 抽象时间线 2837.5 测量及处理时间 2857.6 多处理器的游戏循环 2967.7 网络多人游戏循环 304第8章 人体学接口设备(HID) 3098.1 各种人体学接口设备 3098.2 人体学接口设备的接口技术 3118.3 输入类型 3128.4 输出类型 3168.5 游戏引擎的人体学接口设备系统 3188.6 人体学接口设备使用实践 332第9章 调试及开发工具 3339.1 日志及跟踪 3339.2 调试用的绘图功能 3379.3 游戏内置菜单 3449.4 游戏内置主控台 3479.5 调试用摄像机和游戏暂停 3489.6 作弊 3489.7 屏幕截图及录像 3499.8 游戏内置性能剖析 3499.9 游戏内置的内存统计和泄漏检测 356第三部分 图形及动画 359第10章 渲染引擎 36110.1 采用深度缓冲的三角形光栅化基础 36110.2 渲染管道 40410.3 高级光照及全局光照 42610.4 视觉效果和覆盖层 43810.5 延伸阅读 446第11章 动画系统 44711.1 角色动画的类型 44711.2 骨骼 45211.3 姿势 45411.4 动画片段 45911.5 蒙皮及生成矩阵调色板 47111.6 动画混合 47611.7 后期处理 49311.8 压缩技术 49611.9 动画系统架构 50111.10 动画管道 50211.11 动作状态机 51511.12 动画控制器 535第12章 碰撞及刚体动力学 53712.1 你想在游戏中加入物理吗 53712.2 碰撞/物理中间件 54212.3 碰撞检测系统 54412.4 刚体动力学 56912.5 整合物理引擎至游戏 60112.6 展望:高级物理功能 616第四部分 游戏性 617第13章 游戏性系统简介 61913.1 剖析游戏世界 61913.2 实现动态元素:游戏对象 62313.3 数据驱动游戏引擎 62613.4 游戏世界编辑器 627第14章 运行时游戏性基础系统 63714.1 游戏性基础系统的组件 63714.2 各种运行时对象模型架构 64014.3 世界组块的数据格式 65714.4 游戏世界的加载和串流 66314.5 对象引用与世界查询 67014.6 实时更新游戏对象 67614.7 事件与消息泵 69014.8 脚本 70714.9 高层次的游戏流程 726第五部分 总结 727第15章 还有更多内容吗 72915.1 一些未谈及的引擎系统 72915.2 游戏性系统 730参考文献 733中文索引 737英文索引 755 参考文献 Tomas Akenine-Moller, Eric Haines, and Naty Hoffman. Real-Time Rendering (3rd Edition). Wellesley, MA: A K Peters, 2008. 中译本:《实时计算机图形学(第2版)》,普建涛译,北京大学出版社,2004. Andrei Alexandrescu. Modern C++ Design: Generic Programming and Design Patterns Applied. Resding, MA: Addison-Wesley, 2001. 中译本:《C++设计新思维:泛型编程与设计模式之应用》,侯捷/於春景译,华中科技大学出版社,2003. Grenville Armitage, Mark Claypool and Philip Branch. Networking and Online Games: Understanding and Engineering Multiplayer Internet Games. New York, NY: John Wiley and Sons, 2006. James Arvo (editor). Graphics Gems II. San Diego, CA: Academic Press, 1991. Grady Booch, Robert A. Maksimchuk, Michael W. Engel, Bobbi J. Young, Jim Conallen, and Kelli A. Houston. Object-Oriented Analysis and Design with Applications (3rd Edition). Reading, MA: Addison-Wesley, 2007. 中译本:《面向对象分析与设计(第3版)》,王海鹏/潘加宇译,电子工业出版社,2012. Mark DeLoura (editor). Game Programming Gems. Hingham, MA: Charles River Media, 2000. 中译本:《游戏编程精粹 1》, 王淑礼译,人民邮电出版社,2004. Mark DeLoura (editor). Game Programming Gems 2. Hingham, MA: Charles River Media, 2001. 中译本:《游戏编程精粹 2》,袁国忠译,人民邮电出版社,2003. Philip Dutré, Kavita Bala and Philippe Bekaert. Advanced Global Illumination (2nd Edition). Wellesley, MA: A K Peters, 2006. David H. Eberly. 3D Game Engine Design: A Practical Approach to Real-Time Computer Graphics. San Francisco, CA: Morgan Kaufmann, 2001. 国内英文版:《3D游戏引擎设计:实时计算机图形学的应用方法(第2版)》,人民邮电出版社,2009. David H. Eberly. 3D Game Engine Architecture: Engineering Real-Time Applications with Wild Magic. San Francisco, CA: Morgan Kaufmann, 2005. David H. Eberly. Game Physics. San Francisco, CA: Morgan Kaufmann, 2003. Christer Ericson. Real-Time Collision Detection. San Francisco, CA: Morgan Kaufmann, 2005. 中译本:《实时碰撞检测算法技术》,刘天慧译,清华大学出版社,2010. Randima Fernando (editor). GPU Gems: Programming Techniques, Tips and Tricks for Real-Time Graphics. Reading, MA: Addison-Wesley, 2004. 中译本:《GPU精粹:实时图形编程的技术、技巧和技艺》,姚勇译,人民邮电出版社,2006. James D. Foley, Andries van Dam, Steven K. Feiner, and John F. Hughes. Computer Graphics: Principles and Practice in C (2nd Edition). Reading, MA: Addison-Wesley, 1995. 中译本:《计算机图形学原理及实践──C语言描述》,唐泽圣/董士海/李华/吴恩华/汪国平译,机械工业出版社,2004. Grant R. Fowles and George L. Cassiday. Analytical Mechanics (7th Edition). Pacific Grove, CA: Brooks Cole, 2005. John David Funge. AI for Games and Animation: A Cognitive Modeling Approach. Wellesley, MA: A K Peters, 1999. Erich Gamma, Richard Helm, Ralph Johnson, and John M. Vlissiddes. Design Patterns: Elements of Reusable Object-Oriented Software. Reading, MA: Addison-Wesley, 1994. 中译本:《设计模式:可复用面向对象软件的基础》,李英军/马晓星/蔡敏/刘建中译,机械工业出版社,2005. Andrew S. Glassner (editor). Graphics Gems I. San Francisco, CA: Morgan Kaufmann, 1990. Paul S. Heckbert (editor). Graphics Gems IV. San Diego, CA: Academic Press, 1994. Maurice Herlihy, Nir Shavit. The Art of Multiprocessor Programming. San Francisco, CA: Morgan Kaufmann, 2008. 中译本:《多处理器编程的艺术》,金海/胡侃译,机械工业出版社,2009. Roberto Ierusalimschy, Luiz Henrique de Figueiredo and Waldemar Celes. Lua 5.1 Reference Manual. Lua.org, 2006. Roberto Ierusalimschy. Programming in Lua, 2nd Edition. Lua.org, 2006. 中译本:《Lua程序设计(第2版)》,周惟迪译,电子工业出版社,2008. Isaac Victor Kerlow. The Art of 3-D Computer Animation and Imaging (2nd Edition). New York, NY: John Wiley and Sons, 2000. David Kirk (editor). Graphics Gems III. San Francisco, CA: Morgan Kaufmann, 1994. Danny Kodicek. Mathematics and Physics for Game Programmers. Hingham, MA: Charles River Media, 2005. Raph Koster. A Theory of Fun for Game Design. Phoenix, AZ: Paraglyph, 2004. 中译本:《快乐之道:游戏设计的黄金法则》,姜文斌等译,百家出版社,2005. John Lakos. Large-Scale C++ Software Design. Reading, MA: Addison-Wesley, 1995. 中译本:《大规模C++程序设计》,李师贤/明仲/曾新红/刘显明译,中国电力出版社,2003. Eric Lengyel. Mathematics for 3D Game Programming and Computer Graphics (2nd Edition). Hingham, MA: Charles River Media, 2003. Tuoc V. Luong, James S. H. Lok, David J. Taylor and Kevin Driscoll. Internationalization: Developing Software for Global Markets. New York, NY: John Wiley & Sons, 1995. Steve Maguire. Writing Solid Code: Microsoft's Techniques for Developing Bug Free C Programs. Bellevue, WA: Microsoft Press, 1993. 国内英文版:《编程精粹:编写高质量C语言代码》,人民邮电出版社,2009. Scott Meyers. Effective C++: 55 Specific Ways to Improve Your Programs and Designs (3rd Edition). Reading, MA: Addison-Wesley, 2005. 中译本:《Effective C++:改善程序与设计的55个具体做法(第3版)》,侯捷译,电子工业出版社,2011. Scott Meyers. More Effective C++: 35 New Ways to Improve Your Programs and Designs. Reading, MA: Addison-Wesley, 1996. 中译本:《More Effective C++:35个改善编程与设计的有效方法(中文版)》,侯捷译,电子工业出版社,2011. Scott Meyers. Effective STL: 50 Specific Ways to Improve Your Use of the Standard Template Library. Reading, MA: Addison-Wesley, 2001. 中译本:《Effective STL:50条有效使用STL的经验》,潘爱民/陈铭/邹开红译,电子工业出版社,2013. Ian Millington. Game Physics Engine Development. San Francisco, CA: Morgan Kaufmann, 2007. Hubert Nguyen (editor). GPU Gems 3. Reading, MA: Addison-Wesley, 2007. 中译本:《GPU精粹3》,杨柏林/陈根浪/王聪译,清华大学出版社,2010. Alan W. Paeth (editor). Graphics Gems V. San Francisco, CA: Morgan Kaufmann, 1995. C. Michael Pilato, Ben Collins-Sussman, and Brian W. Fitzpatrick. Version Control with Subversion (2nd Edition). Sebastopol , CA: O'Reilly Media, 2008. (常被称作“The Subversion Book”,线上版本.) 国内英文版:《使用Subversion进行版本控制》,开明出版社,2009. Matt Pharr (editor). GPU Gems 2: Programming Techniques for High-Performance Graphics and General-Purpose Computation. Reading, MA: Addison-Wesley, 2005. 中译本:《GPU精粹2:高性能图形芯片和通用计算编程技巧》,龚敏敏译,清华大学出版社,2007. Bjarne Stroustrup. The C++ Programming Language, Special Edition (3rd Edition). Reading, MA: Addison-Wesley, 2000. 中译本《C++程序设计语言(特别版)》,裘宗燕译,机械工业出版社,2010. Dante Treglia (editor). Game Programming Gems 3. Hingham, MA: Charles River Media, 2002. 中译本:《游戏编程精粹3》,张磊译,人民邮电出版社,2003. Gino van den Bergen. Collision Detection in Interactive 3D Environments. San Francisco, CA: Morgan Kaufmann, 2003. Alan Watt. 3D Computer Graphics (3rd Edition). Reading, MA: Addison Wesley, 1999. James Whitehead II, Bryan McLemore and Matthew Orlando. World of Warcraft Programming: A Guide and Reference for Creating WoW Addons. New York, NY: John Wiley & Sons, 2008. 中译本:《魔兽世界编程宝典:World of Warcraft Addons完全参考手册》,杨柏林/张卫星/王聪译,清华大学出版社,2010. Richard Williams. The Animator's Survival Kit. London, England: Faber & Faber, 2002. 中译本:《原动画基础教程:动画人的生存手册》,邓晓娥译,中国青年出版社,2006. 勘误 第1次印册(2014年2月) P.xviii: 译注中 Wholesale Algoithms -> Wholesale Algorithms P.10: 最后一段第一行 微软的媒体播放器 -> 微软的Windows Media Player (多谢读者OpenGPU来函指正) P.15: 1.4.3节第三点 按妞 -> 按钮 (多谢读者一个小小凡人来函指正) P.40: 正文最后一行 按扭 -> 按钮 P.50: 1.7.8节第二节第一行 同是 -> 同时 (多谢读者czfdd来函指正) P.98: 代码 writeExampleStruct(Example& ex, Stream& ex) 中 Stream& ex -> Stream& stream (多谢读者Snow来函指正) P.106: 第一段中有六处 BBS -> BSS,最后一段代码的注释也有同样错误 (多谢读者trout来函指正) P.119: 译注中 软体工程 -> 软件工程 (多谢读者Snow来函指正) P.214: 正文第一段有两处 虚内存 -> 虚拟内存 (多谢读者Snow来函指正) P.216: 脚注24应标明为译注 (多谢读者Snow来函指正) P.221: 第一段代码的第二个断言应为 ASSERT(link.m_pPrev != NULL); (多谢读者Snow来函指正) P.230: 5.4.4.1节 第二段 软体 -> 软件 P.286: 脚注4应标明为译注 (多谢读者Snow来函指正) P.322: 第二段 按扭事件字 -> 按钮事件 P.349: 9.8节第二段第二行两处 部析器 -> 剖析器 (多谢读者Snow来函指正) P.738-572: 双数页页眉 参考文献 -> 中文索引 P.755-772: 双数页页眉 参考文献 -> 英文索引 P.755: kd tree项应归入K而不是Symbols 以上的错误已于第2次印册中修正。 第2次印册及之前 P.11: 第四行 细致程度 -> 层次细节 (这是level-of-detail/LOD的内地通译,多谢读者OpenGPU来函指正) P.12: 正文第一段及图1.2标题 使命之唤 -> 使命召唤 (多谢读者OpenGPU来函指正) P.12: 正文第一段 战栗时空 -> 半条命 (多谢读者OpenGPU来函指正) P.16: 第一点 表面下散射 -> 次表面散射 (多谢读者OpenGPU来函指正) P.17: 1.4.4节第五行 次文化 -> 亚文化 (此译法在内地更常用。多谢读者OpenGPU来函提示) P.22: 战栗时空 -> 半条命 P.24: 战栗时空2 -> 半条命2 P.34: 1.6.8.2节第一行 提呈 -> 提交 (这术语在本书其他地方都写作提交。多谢读者OpenGPU来函提示) P.35: 第七行 提呈 -> 提交 (这术语在本书其他地方都写作提交。多谢读者OpenGPU来函提示) P.50: 战栗时空2 -> 半条命2 P.365: 第四段第二行: 细致程度 -> 层次细节 P.441: 10.4.3.2节第三行 细致程度 -> 层次细节 P.494: sinusiod -> sinusoid (多谢读者OpenGPU来函指正) P.511: 11.10.4节第一行 谈入 -> 淡入 (多谢读者Snow来函指正) P.541: 战栗时空2 -> 半条命2 P.627: 战栗时空2 -> 半条命2 P.654: 第二行 建康值 -> 血量 (原来是改正错别字,但译者发现应改作前后统一使用的“血量”。多谢读者Snow来函指正) P.692: 第二行 内部分式 -> 内部方式 (多谢读者Snow来函指正) P.696: 14.7.6节第四行 不设实际 -> 不切实际 (多谢读者Snow来函指正) 以上的错误已于第3次印册中修正。 其他意见 P.220: 正文第一段 m_root.m_pElement 和 P.218 第一段代码中的 m_pElem 不统一。原文有此问题,但因为它们是不同的struct,暂不列作错误。 (多谢读者Snow来函提示) P.331: 8.5.8节第二段中 “反覆”较常见的写法为“反复”,但前者也是正确的,暂不列作错误。 (多谢读者Snow来函提示) P.390: 10.1.3.3节静态光照第二段中“取而代之,我们会使用一张光照纹理贴到所有受光源影响范围内的物体上。这样做能令动态物体经过光源时得到正确的光照。” 后面的一句与前句好像难以一起理解。译者认为,作者应该是指,使用同一静态光源去为静态物件生成光照纹理,以及用于动态对象的光照,能使两者的效果维持一致性。译者会考虑对译文作出改善或加入译注解译。(多谢读者店残来函查询) P.689: 第五行 并行处理世代 -> 并行处理时代 是对era较准确的翻译。 (多谢读者Snow来函提示) 本篇文章为转载内容。原文链接:https://blog.csdn.net/mypongo/article/details/38388381。 该文由互联网用户投稿提供,文中观点代表作者本人意见,并不代表本站的立场。 作为信息平台,本站仅提供文章转载服务,并不拥有其所有权,也不对文章内容的真实性、准确性和合法性承担责任。 如发现本文存在侵权、违法、违规或事实不符的情况,请及时联系我们,我们将第一时间进行核实并删除相应内容。
2023-02-12 23:04:05
327
转载
站内搜索
用于搜索本网站内部文章,支持栏目切换。
知识学习
实践的时候请根据实际情况谨慎操作。
随机学习一条linux命令:
history | tail -n 10
- 查看最近使用的10条命令历史。
推荐内容
推荐本栏目内的其它文章,看看还有哪些文章让你感兴趣。
2023-04-28
2023-08-09
2023-06-18
2023-04-14
2023-02-18
2023-04-17
2024-01-11
2023-10-03
2023-09-09
2023-06-13
2023-08-07
2023-03-11
历史内容
快速导航到对应月份的历史文章列表。
随便看看
拉到页底了吧,随便看看还有哪些文章你可能感兴趣。
时光飞逝
"流光容易把人抛,红了樱桃,绿了芭蕉。"