首页 文章 精选 留言 我的

精选列表

搜索[思考模型],共10000篇文章
优秀的个人博客,低调大师

每日一博 | 聊聊各端手势体系以及对 Web 标准手势的思考

「北海 Kraken」是一款基于 Flutter 的 Web 渲染引擎,通过基于 W3C 标准来开发实现前端开发者常用的能力。 Kraken 团队也积极探索定义新的问题以及能力,期望通过参推动标准定制的方式让 Web 技术变得更好。 欢迎大家关注 「北海 Kraken」: http://openkraken.com/ 在过去,早期的 Web 更多用做内容展示的页面,最早从后端框架中直出,再配上各种 CSS 以及 JS 的交互内容,以完成最终对页面内容的展示,那时候的 Web 更多属于 【内容开发】,做内容的直出与展示。而如今现代 Web 开发体系已经有了翻天覆地的变化,早已超出了【内容开发】的范畴,在各个领域都有 JavaScript 的身影。同样,Web 也已经脱离了客户端以及浏览器的限制,各式各样基于 Web 标准或者私有标准的 Web runtime 层出不穷。【Web 应用开发】区别于传统的【内容开发】,它对开发者提出了更高的要求,也对 Web 的能力提出了更高的要求,无论是基于标准化方面的考量,还是基于对易用性的考虑,我们都期望 Web 开发者可以获得通过更高级的封装的标准的高性能的能力。 而手势能力就是其中的一块。 目前在 Web 标准中,手势能力是属于缺失的一块能力,更多的开发者通过 hammer.js 来获得一个通过 JavaScript 模拟出来的手势事件来开发一个手势强交互的应用,或者是直接基于更底层的 Touch event来做进一步的封装。 但是无论是类 hammer.js 的前端手势方案,还是 Touch event的封装,都会导致一些问题,我将从易用性、性能、标准化的角度来做进一步的分析: 易用性: 开发者必须手动去实现或者封装更高一级的手势能力,无法直接从 element 上获得某个高级手势的 event 事件。无论是开发成本,都需要额外加载或者执行额外的 CDN,都是对前端资源的一种损耗。 性能: 通过 JavaScript 实现的方案需要频繁地通过 Bridge 将手势的能力传递到前端,然后再去计算模拟相关的手势事件。频繁的传递数据增加了 Bridge 的消耗,不断执行的 JavaScript 会阻塞 UI 线程,如果需要更强大的手势能力支撑,我们必须进一步封装【竞争场】等能力的实现来达到手势竞争的目的,而这部分能力本应下沉到渲染引擎本身,而不是在 JavaScript 中处理。 标准化: 各个开发者实现的标准不统一,判断的基准不一致,透出的 event 能力不对齐会导致各个平台甚至到各个页面的标准不统一。譬如说在同一个 iOS 设备上访问两个不同开发者开发的页面,不统一的手势能力可能会给用户带来及其糟糕的体验。非标准化的手势能力在各个端上也显得格外突出,下面我介绍各端的手势能力时会介绍这些差异点。 连续手势与离散手势 首先我需要介绍一下连续手势与离散手势的概念,以便读者可以更好地区分这两种手势的不同,以及了解实现不同的手势能力对开发、性能、易用性等纬度的影响。 首先,我们需要知道,由于在端侧有各种各样的屏幕操作的设备,常见的比如说类 apple pencil 的电子笔设备(pen),手指直接触摸操作(touch),还有鼠标(mouse)等。所以在 W3C 标准中, 将所有的接触屏幕的物理设备抽象成了一个 pointer,无论上层是那种物理设备,对于屏幕只感知与抽象这一个触摸到的点,基于 type 区分具体的上层的物理设备。 一个完整的手势包含了手指开始接触屏幕(pointer down),然后手指在屏幕上进行偏移(pointer move),以及手指抬起离开屏幕(pointer up),暂时不讨论 cancel、out 等情况。当然,其中中间 pointer move 的过程是可以省略的,最常见的省略 pointer move 的手势譬如说 click 或者 long press 等(当然,如果点击设备不是一个鼠标而是一根手指,其实手指实际接触是肯定会产生轻微移动情况的,譬如在 FLutter 中,允许这个细微的移动距离在 18 个像素点内,即视为不移动)。可以预见的是未来会有更多的物理设备操作屏(甚至不是屏),基于底层触摸点的 pointer 抽象有利于上层做更多的扩展。 了解了这些,接下来我们来了解一下连续手势与离散手势的差异。 连续手势:从 pointer down 到 pointer moves 到 pointer up,中间过程可以通过 state 状态来描述的手势,可以清楚地通过不同的回调或者不同的状态让开发者感知目前手势所处的状态的手势。常见连续手势:pan。 离散手势:完整手势触发完毕后才会通过回调来通知开发者,无中间状态的转换。常见离散手势:click。 连续手势会频繁通过回调或者状态来通知开发者目前手势所处的状态,我们来看一种情况: element.addEventLisenter('pan', (gestureEvent) => { if (gestureEvent.state === 'up') { // do something... } }) 假设我们需要在 Web 标准中实现 pan 这个手势,如果它是一个连续手势,而我们的场景只需要用 up 这种状态,就需要不断地将当前的状态通过 Bridge 以及 JS engine 传递到 JavaScript 中,这频繁的传递开销是对设备性能的一种浪费。当然,也有框架方案通过更加细分的粒度去解决这个事情,譬如说拆分成 panstart、panupdate、panend等,当开发者不给这些方法注册回调时,可以在框架内部判断并做相应优化。然而细分的 API 抽象不够底层,对于开发者来说也并不那么友好。 而对于离散手势,我们则不需要考虑手势过程中的状态传递,只需要把最终的结果返回给开发者即可,离散手势屏蔽了许多内部处理的细节,保证了开发者注册的回调只能完整的手势操作完以后才能被命中。有效地降低了连续手势数据的传递量。但是相较于连续手势,离散手势的缺点是开发者无法很好地感知中间状态。 接下来我们来看一下各个端上实现的手势体系、优缺点以及差异性。 各端手势体系 hammer.js Pan Pan 的需要有一个最小距离以及方向,只要达到这个条件可以频繁触发。 详见:https://github.com/hammerjs/hammer.js/blob/master/src/recognizers/pan.js#L42 Swipe 距离与速度达到一定阈值的滑动。 详见:https://github.com/hammerjs/hammer.js/blob/master/src/recognizers/swipe.js#L36 Pinch (向外捏时放大,向内捏时缩小) Press (长按,默认时间在 250 ms 以上 ,替代了 long press) 详见:https://github.com/hammerjs/hammer.js/blob/master/src/recognizers/press.js#L78 hammer.js 作为一个前端实现的 gesture lib,通过注册 Touch 事件做封装来完成具体的操作的判断,在前端做手势的方案在前面已经提过,需要不断地通过 Bridge 以及 JS engine 传递到 JavaScript 中,然后才能最终在 JavaScript 中处理手势操作,只要有操作就会被抛到 JavaScript 中进行处理,频繁的传递耗费了许多不必要的性能。我们更希望这部分能力可以下沉到渲染引擎本身,这样可以节省非常多不必要的数据传递开销。 如果在基础手势判断之上想进一步引入更加复杂的【竞技场】等能力,这部分会使得 JavaScript 中的逻辑更加复杂,即便抛开“能不能”在前端做相关实现来说,过多的 JavaScript 运行占用计算资源也是我们并不想要的。 同时,需要单独引入一个 CDN 脚本来支持相关的功能,对于包体积以及首屏也增加了额外的成本。但是又考虑到本身浏览器并不自带这些功能,一般开发者也无法很好地将这套方案优化并下沉到浏览器中,所以在反而在大部分前端业务场景成为来较优的技术选型。 Flutter Tap Double tap Long press (500 ms 以上的长按) Vertical(Horizontal) drag 横(纵)滑 在 drag 上做了进一步封装,在 x 轴或者 y 轴偏移超过最小距离并达到阈值速度可触发。 scale scale 会包含放大缩小以及旋转的手势,相当于其他端中的 Pinch + Rotation Pan Pan 内部实现,需要达到一个最小速度以及最小移动距离的 drag 才能触发 Pan,Pan 是基于 drag 之上的封装,增加了判断。Flutter 内置一个 pointer down + pointer moves + pointer up 只能触发一次手势,所以 Pan 只能触发一次(与 hammer 不同,为有状态手势) 详见: https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/gestures/monodrag.dart#L579 Flutter 的手势体系除了 Long press 均为连续手势,无离散手势,Tap 也会通过 TapDown、TapUp 等状态来完成。每个中间状态会通过不同的回调函数来支持开发者处理逻辑,返回参数也根据中间状态的不同而不同。排除 Widget 的统一封装来看,跟安卓类似,回调过多,且返回参数不统一,不利于标准化。但是对于内部的手势回调来说,细分的接口各司其职,传递所需的参数都是必须的,开发者可以直接获取具体的返回信息。 iOS Tap(离散手势,100 ms 左右的点击行为) Long Press (连续手势,500 ms 以上的点击行为) Pan (连续手势,平移,类似 drag,但是可以在移动过程中不断变化方向) Swipe (离散手势) Pinch(连续手势,向外捏时放大,向内捏时缩小) Rotation(连续手势,旋转) 为了方便大家了解各个手势的区别,尤其是 Pan 跟 Swipe 的区别,特地放上了iOS 开发者文档的一些图片。 iOS 的手势可以带上多个 touch pointer,同时满足了几个手指操作的能力。比如三指滑动(三根手指的swipe)、双指点击(两根手指的 Tap)等。它提供了开发者对某一个手势处理成一个注册回调函数,通过 state 判断目前手势的状态。离散手势与连续手势共存。 Android View 上直接提供 click 以及 touch 的一些方法 OnDragListener:拖动事件。 OnLongClickListener:长按抬起时的事件。 GestureDetector.OnGestureListener onDown:手势识别器的 down 事件。 onFling: 类似 swipe。 onLongPress: 长按。 onScroll:scroll view 滚动时的事件。 onShowPress:按下后没抬起,相当于(up、move、down 的中间 move 状态,只是没move)。 onSingleTapUp: 点击抬起,对应 onDown。 GestureDetector.OnDoubleTapListener: 双击。 ScaleGestureDetector:旋转,捏,分 begin、onScale、end。 相对来说,Android 的手势体系比较细分,大致上跟 FLutter 比较像,但是 Flutter 是不同手势在不同类中的,Flutter 基本上都是离散手势,安卓很多连续手势,但是更加细分。 标准 综上分析了 Flutter、iOS、Android 以及一个前端实现的 gesture lib(hammer.js),不难发现,每个端实现的手势方案都大同小异。无非都实现了这几种方法: click(Tap)、swipe、Pan、Long Press (Press)、Pinch 与 Rotation(或者 Scale)。但是各个平台对每个手势的实现还是有些许的差异,无论是具体手势的代码逻辑判断还是具体手势的拆分或者命名,均有不同。 那在 Web 技术上,我们应该使用怎么样一套手势规范,来兼顾易用性、性能以及标准化呢?就目前来看,基于 Web 技术体系发展来的 Web runtime 的已经非常多了,诸如 Web、React Native、小程序等体系已经在端侧带来了巨大的运行时碎片化。未来不止于移动端上,还有各种 IOT 设备出现,可能会有越来越多的 Web runtime 会出现。未来可能会有更多领域会有不一样的终端设备,而折叠屏、柔性屏的到来也可能会让端侧的设备(手机、IOT、车载等)形成更多更复杂的的跨端场景,随之而来的也是更多的交互手势来与这些设备进行“沟通交流”。 很遗憾的是目前 W3C 上没有相应的手势规范,我们更期望有一个统一的既定标准来规范,我们也在 W3C 中文兴趣小组上发起了一个讨论,目前此讨论已经提到了 UIEvent。我们期望通过易用性、性能以及标准化这几个纬度去讨论手势规范以及对应的手势标准化能力的必要性,以及最终推动规范建立的可行性,也欢迎更多的小伙伴加入该讨论。 此外,该标准提案目前已经在 北海 Kraken 上实现,开发者可以直接使用 增强的手势能力 来开发复杂的交互应用。后续 北海 Kraken 团队将会在复杂的业务场景上定义出更多问题以及通用能力,期望可以通过参与推动标准定制的方式让 Web 技术变得更好。

优秀的个人博客,低调大师

一次线上故障:数据库连接池泄露后的思考

云栖号:https://yqh.aliyun.com第一手的上云资讯,不同行业精选的上云企业案例库,基于众多成功案例萃取而成的最佳实践,助力您上云决策! 一:初步排查 早上作为能效平台系统的使用高峰期,系统负载通常比其它时间段更大一些,某个时间段会有大量用户登录。当天系统开始有用户报障,发布系统线上无法构建发布,然后后续有用户不能登录系统,系统发生假死,当然系统不是真的宕机,而是所有和数据库有关的连接都被阻塞,随后查看日志发现有大量报错。 和数据库连接池相关: Caused by: org.springframework.jdbc.CannotGetJdbcConnectionException: Failed to obtain JDBC Connection; nested exception is java.sql.SQLTransientConnectionException: HikariPool-1 - Connection is not available, request timed out after 30002ms. 可以看出上面的报错和数据库连接有关,大量超时。通过对线上debug日志的分析,也验证了数据库连接池被大量消耗。 [DEBUG] c.z.h.p.HikariPool: HikariPool-1 - Timeout failure stats (total=20, active=20, idle=0, waiting=13) 这是开始大量报错前的日志。我们可以看到此时HikariPool连接池已经无法获取连接了,active=20表示被获取正在被使用的数据库连接。waiting表示当前正在排队获取连接的请求数量。可以看出,已经有相当多的请求处于挂起状态。 所以当时我们的解决办法是调整数据库连接池大小,开始初步认为是,高峰时期,我们设置的连接池数量大小,不足以支撑早高峰的连接数量导致的。 jdbc.connection.timeout=30000 jdbc.max.lifetime=1800000 jdbc.maximum.poolsize=200 jdbc.minimum.idle=10 jdbc.idle.timeout=60000 jdbc.readonly=false 我们将将数据库连接池的数量调整到了200。 二:事务 2.1事务滥用的后果 及时将配置调整成了200,服务重启也恢复了正常,但是我仍然认为系统存在连接泄露的风险,我试图从日志表现出的行为里寻找蛛丝马迹。我在访问日志看到每次在系统崩溃前,其实都有人在做构建,而且构建经常点击没反应,我当时添加的构建debug日志也显示了这一点。我开始怀疑是构建造成的连接泄露。 在这里我简单说下构建代码处的逻辑 用户触发构建 将job加入增量job缓存,用于更新job状态 jenkinsClient调用jenkins的api,开始构建 将构建信息写入数据库(jobname,version) 我开始观察自己写的代码,可是看了多遍,我也发现不了这段代码和数据库连接有啥关系,大多数人包括当时自己来说,数据库连接的泄露,大多数情况应该是服务和数据库连接的过程中发生了阻塞,导致连接泄露。但是现在来看,很容易能发现问题所在,看当时的代码: @Transactional(rollbackFor = Exception.class) public void build(BuildHistoryReq buildHistoryReq) { //1.封装操作 //2.调用jenkins Api //3.数据库更新写入 } 这就是当时的代码入口,当然代码处没有这么简单。可以看到我在方法入口就加上了Transactional注解,这里的意思其实就是发生错误,抛出异常时,数据库回滚。 问题就出现在了这里,当有用户点击构建时,请求刚进入build方法时,就会从数据库连接获取一个连接。可是此时,程序并没有和数据库相关的操作,如果此时代码在步骤1或者2处出现io或者网络阻塞,就会导致,事务无法提交,连接也就会一直被该请求占用。而再大的连接池也会被耗费殆尽。从而造成系统崩溃。 2.2事务注解的正确用法 通常情况下作为非业务部门,没有涉及到核心的业务,像支付,订单,库存相关的操作时,事务在可读层面并没有特别高的要求。通常也只涉及到,多表操作同时更新时,保证数据一致性,要么同时成功要么同时失败。而使用 @Transactional(rollbackFor = Exception.class) 足以。 而上述代码该如何改进呢?? 首先分析有没有需要使用事务的必要。在步骤3中,数据操作,看代码后发现只有对一张表的操作,同时和其它操作没有相关性。而且本身属于最后一个步骤。所以在此代码中完全没有必要使用,删除注解即可。 当然如果步骤3操作数据库是多表操作,具有强相关性,数据一致,我们可以这样做。将和步骤3无关的步骤分开,变成两个方法,那么在1,2处发生阻塞也不会影响到数据库连接。 public void build(BuildHistoryReq buildHistoryReq) { //1.封装操作 //2.调用jenkins Api update**(XX); } @Transactional(rollbackFor = Exception.class) public void update**(XX xx) { //3.数据库更新写入 } 这里需要注意,注解事务的用法,方法必须是公开调用的。 三:HttpClient 4.x连接池 当时找到数据连接池泄露的原因后,我第一步就是去掉了事务,然后加上了一些日志,这时我已经能确定代码在jenkinsclient处出现了问题,但是仍然不确定问题出在了哪,我只能加上一些日志,同时通过监控继续观察。 果然在hotfix的第二天还是出现了我预料中的事情,构建发布仍然有问题,当然此时其它功能是不受影响了。我观察日志发现构建开始并在该处阻塞 jenkinsClient.startBuild(jobName, params); 随后我观察了项目监控。观察线程情况,发现大量http-nio的线程阻塞了,而这个线程和httpclient相关。 java.lang.Thread.State: WAITING (parking) at sun.misc.Unsafe.park(Native Method) - parking to wait for <0x00000007067027e8> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject) at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175) at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039) at org.apache.http.pool.AbstractConnPool.getPoolEntryBlocking(AbstractConnPool.java:379) at org.apache.http.pool.AbstractConnPool.access$200(AbstractConnPool.java:69) at org.apache.http.pool.AbstractConnPool$2.get(AbstractConnPool.java:245) - locked <0x00000007824713a0> (a org.apache.http.pool.AbstractConnPool$2) at org.apache.http.pool.AbstractConnPool$2.get(AbstractConnPool.java:193) 随后我跟进源码查看了AbstractConnPool类的379行 可以看到线程走到379行执行了this.condition.await()后进入无限期的等待,所以此时如果没有线程执行this.condition.signal()就会导致该线程一直处于waiting状态,而前端也会迟迟收不到相应,导致请求timeout。 我们再分析下源码,看看什么情况下会导致线程跑到该处: /** * 获取http连接,从名称也能看出该方法会造成阻塞 */ private E getPoolEntryBlocking( final T route, final Object state, final long timeout, final TimeUnit timeUnit, final Future<E> future) throws IOException, InterruptedException, TimeoutException { Date deadline = null; if (timeout > 0) { deadline = new Date (System.currentTimeMillis() + timeUnit.toMillis(timeout)); } this.lock.lock(); try { final RouteSpecificPool<T, C, E> pool = getPool(route); E entry; for (;;) { Asserts.check(!this.isShutDown, "Connection pool shut down"); for (;;) { entry = pool.getFree(state); if (entry == null) { break; } if (entry.isExpired(System.currentTimeMillis())) { entry.close(); } if (entry.isClosed()) { this.available.remove(entry); pool.free(entry, false); } else { break; } } if (entry != null) { this.available.remove(entry); this.leased.add(entry); onReuse(entry); return entry; } // New connection is needed final int maxPerRoute = getMax(route); // Shrink the pool prior to allocating a new connection final int excess = Math.max(0, pool.getAllocatedCount() + 1 - maxPerRoute); if (excess > 0) { for (int i = 0; i < excess; i++) { final E lastUsed = pool.getLastUsed(); if (lastUsed == null) { break; } lastUsed.close(); this.available.remove(lastUsed); pool.remove(lastUsed); } } if (pool.getAllocatedCount() < maxPerRoute) { final int totalUsed = this.leased.size(); final int freeCapacity = Math.max(this.maxTotal - totalUsed, 0); if (freeCapacity > 0) { final int totalAvailable = this.available.size(); if (totalAvailable > freeCapacity - 1) { if (!this.available.isEmpty()) { final E lastUsed = this.available.removeLast(); lastUsed.close(); final RouteSpecificPool<T, C, E> otherpool = getPool(lastUsed.getRoute()); otherpool.remove(lastUsed); } } final C conn = this.connFactory.create(route); entry = pool.add(conn); this.leased.add(entry); return entry; } } boolean success = false; try { if (future.isCancelled()) { throw new InterruptedException("Operation interrupted"); } pool.queue(future); this.pending.add(future); if (deadline != null) { success = this.condition.awaitUntil(deadline); } else { this.condition.await(); success = true; } if (future.isCancelled()) { throw new InterruptedException("Operation interrupted"); } } finally { // In case of 'success', we were woken up by the // connection pool and should now have a connection // waiting for us, or else we're shutting down. // Just continue in the loop, both cases are checked. pool.unqueue(future); this.pending.remove(future); } // check for spurious wakeup vs. timeout if (!success && (deadline != null && deadline.getTime() <= System.currentTimeMillis())) { break; } } throw new TimeoutException("Timeout waiting for connection"); } finally { this.lock.unlock(); } } 从源码我们可以看出有几处必要条件才会导致线程会无限期等待: timeout=0 也就是说没有给默认值,导致: deadline = null pool.getAllocatedCount() < maxPerRoute 判断是否已经到达了该路由(host地址)的最大连接数。 其实整体逻辑就是,从池里获取连接,如果有就直接返回,没有,判断当前请求出去的路由有没有到达该路由的最大值,如果达到了,就进行等待。如果timeout为0就会进行无限期等待。 而这些值我本身也没有做任何设置,我当时的第一想法就是,给http请求设置超时时间。也就是给每个client设置必要的参数 解决 1.jenkinsClient分配超时时间 public HttpClientBuilder clientBuilder() { HttpClientBuilder httpClientBuilder = HttpClientBuilder.create(); RequestConfig.Builder builder = RequestConfig.custom(); //该参数对应AbstractConnecPool getPoolEntryBlocking方法的timeout builder.setConnectionRequestTimeout(5 * 1000); //数据传输的超时时间 builder.setSocketTimeout(20 * 1000); //该参数为,服务和jenkins连接的时间(通常连接的时间都很短,可以设置小点) builder.setConnectTimeout(5 * 1000); httpClientBuilder.setDefaultRequestConfig(builder.build()); return httpClientBuilder; } 2.构建JenkinsClient和更新使用的JenkinsClient分离 其实我已经尝试用池化的思想来解决该问题了。 诡异bug(同一个JenkinsClient,调用不同的api,有的api会阻塞,有的调用仍然正常) 但hotfix的第二天,又出现了一个诡异的bug: 构建可以,但是无法同步job的状态。这里出现这个问题的原因在于我将构建和更新两个过程使用的jenkinsClient分离成两个,所以这个过程相互独立,互不影响,所以,更新的client出了问题但是构建的client仍然能正常使用。 但是更新过程的JenkinsClient出现的问题让我百思不得其解。我们先看看更新状态过程会使用到的api(接口) //获取对应的job 1 JobWithDetails job = client.get(UrlUtils.toJobBaseUrl(folder, jobName), JobWithDetails.class); //获取job构建的pipeline流水 2 client.get("/job/" + EncodingUtils.encode(jobName) + "/" + version + "/wfapi/describe", PipelineStep.class); //获取对应job某次build的详情 3 client.get(url, BuildWithDetails.class); bug问题1:为什么全量更新job和增量更新job使用的是同一个JenkinsClient,但是全量更新仍然正常获取值,而增量更新job状态的线程确出现阻塞超时(超时是因为前面我设置了timeout,使得请求不会一直阻塞下去)。 要回答这个问题,就要回到线程的相关问题了, this.condition.wait()会导致当前线程阻塞,并不会影响到另外线程。而更新使用了两个线程。所以这个问题也比较好回答。 bug问题2:为什么同一个线程(增量更新job线程)调用不同api,有的成功,而有的会阻塞: 解决这个问题,我们还是得回到AbstractConnPool中的方法getPoolEntryBlocking()来看: if (pool.getAllocatedCount() < maxPerRoute) { } 当前请求的路由如果已经达到最大值了就会阻塞等待。那么同一个jenkinsclient,按理来说不可能会出现不同的路由。所以同一个client要么都能访问,要么都会阻塞,怎么会出现有的能访问有的会阻塞。为了寻求问题的答案,我翻阅了JenkinsClient的源码,结合日志,发现服务每次阻塞的方法是: 不管多少次,每次都会完美的在该地方阻塞:对应上面的api 3: //获取对应job某次build的详情 3 client.get(url, BuildWithDetails.class); 这个url和其它两个api拿到的路由都有区别:可以跟随我一起看源码: public class Build extends BaseModel { private int number; private int queueId; private String url; } 我们可以看到url是属于Build的属性,并非client我们设置的值,当然有人会觉得该值可能是通过将配置的url设置过来的。我们可以接着看,哪些方法可能会给build设置url,三个构造函数,一个set方法都可以,如果我们继续只看源码仍然很难找到问题所在,所以这时候我开始启动服务debug; 发现了问题在哪: 可以看出调用jenkins的这个api出现了两个router,也可以看出这个url是jenkins返回的,查阅资料可以看到,jenkins系统设置时可以设置这个url。 所以这个bug也能很好的解释了,对于httpclient来说,每个router默认可以最多两个连接。虽然是同一个调用api采用的是同一个jenkinsClient,但是却维护了两个router,一个是从配置中获取,一个是jenkins返回的,这个是配置不一致导致的。 JenkinsClient分配连接数: public HttpClientBuilder clientBuilder() { HttpClientBuilder httpClientBuilder = HttpClientBuilder.create(); RequestConfig.Builder builder = RequestConfig.custom(); builder.setConnectionRequestTimeout(5 * 1000); builder.setSocketTimeout(20 * 1000); builder.setConnectTimeout(5 * 1000); httpClientBuilder.setDefaultRequestConfig(builder.build()); //每个路由最多有10个连接(默认2个) httpClientBuilder.setMaxConnPerRoute(10); //设置连接池最大连接数 httpClientBuilder.setMaxConnTotal(20); return httpClientBuilder; } 给JenkinsClient添加健康检查,并手动更新不能用的Client @Slf4j public class JenkinsClientManager implements Runnable { private volatile boolean flag = true; private final JenkinsClientProvider jenkinsClientProvider; public JenkinsClientManager(JenkinsClientProvider jenkinsClientProvider) { this.jenkinsClientProvider = jenkinsClientProvider; } @Override public void run() { while (flag) { try { checkJenkinsHealth(); //每30秒检查一次 Thread.sleep(30_000); } catch (Exception e) { log.warn("check health error:{}", e.getMessage()); } } } public void checkJenkinsHealth() { log.debug("check jenkins client health start"); //获取client是否可用 available = isAvailable(..) if (!available || !queryAvailable) { //更新client jenkinsClientProvider.retrieveJenkinsClient(); } } private boolean isAvailable(Set<Map.Entry<String, JenkinsClient>> entries) { boolean available = true; for (Map.Entry<String, JenkinsClient> entry : entries) { boolean running = entry.getValue().isRunning(); if (!running) { log.debug("jenkins running error"); available = false; } } return available; } @PostConstruct public void start() { TaskSchedulerConfig.getExecutor().execute(this); } } 四:JenkinsClient连接池 采用池化技术解决client高可用和重复利用问题 虽然我手动写了一个JenkinsClientManager每30秒来维护一次client,但是这种手工的方式并不好: 每30秒维护一次,若是在期间发生问题,那么只能干等 无法动态的根据系统需要,动态构建新的client,也就是无法满足高并发下的使用问题 无法配置 目前我们都知道各种池化技术:线程池、数据库连接池、redis连接池。 笔者在实现jenkinsClient pool之前,参考了线程池、数据库连接池的实现、发现其底层实现较为复杂、redis的连接池技术相对来说容易看懂和学习、所以采用了和jedis一样的实现方式来实现JenkinsClient的连接池 这是jedis的类结构目录,其实重点在我标记的这5个类。 jedis本身也是采用的commons-pool2提供的池技术实现的,接下来我会简单介绍一下该工具提供的池化技术。 JenkinsClient连接池应该要具备哪些功能?? 动态创建JenkinsClient 使用完的Client放回池中 回收长期不用和不可用的Client 能够根据需要配置一定数量的Client 对于提到的这些功能,我将通过commons-pool2包来实现 PooledObjectFactory:该接口管理着bean的生命周期(An interface defining life-cycle methods for instances to be served by an) makeObject 方法创建一个可以入池的实例,也就是我们需要用的Client由该方法创建 destroyObject 方法可以销毁不可用或者过期的对象 validateObject 方法对实例进行验证,在每次创建完实例后,都会调用该方法,同时也会以一定的频率进行健康检查(频率timeBetweenEvictionRunsMillis) GenericObjectPool:实例都会放入该池中进行管理: //所有的可用连接 private final Map<IdentityWrapper<T>, PooledObject<T>> allObjects = new ConcurrentHashMap<>(); //空闲的可用连接 private final LinkedBlockingDeque<PooledObject<T>> idleObjects; //获取可用连接 T borrowObject() throws Exception, NoSuchElementException, IllegalStateException; //资源释放(将连接放回连接池) void returnObject(T obj) throws Exception; 配置(BaseObjectPoolConfig,但是我们继承GenericObjectPoolConfig,该类给出了大量的默认值) 链接池中最大连接数,默认为8 maxTotal #链接池中最大空闲的连接数,默认也为8 maxIdle #连接池中最少空闲的连接数,默认为0 minIdle #连接空闲的最小时间,达到此值后空闲连接将可能会被移除。默认为1000L*60L*30L minEvictableIdleTimeMillis #连接空闲的最小时间,达到此值后空闲链接将会被移除,且保留minIdle个空闲连接数。默认为-1 softMinEvictableIdleTimeMillis #当连接池资源耗尽时,等待时间,超出则抛异常,默认为-1即永不超时 maxWaitMillis #当这个值为true的时候,maxWaitMillis参数才能生效。为false的时候,当连接池没资源,则立马抛异常。默认为true blockWhenExhausted #空闲链接检测线程检测的周期,毫秒数。如果为负值,表示不运行检测线程。默认为-1. timeBetweenEvictionRunsMillis #在每次空闲连接回收器线程(如果有)运行时检查的连接数量,默认为3 numTestsPerEvictionRun #默认false,create的时候检测是有有效,如果无效则从连接池中移除,并尝试获取继续获取 testOnCreate #默认false,borrow的时候检测是有有效,如果无效则从连接池中移除,并尝试获取继续获取 testOnBorrow #默认false,return的时候检测是有有效,如果无效则从连接池中移除,并尝试获取继续获取 testOnReturn #默认false,在evictor线程里头,当evictionPolicy.evict方法返回false时,而且testWhileIdle为true的时候则检测是否有效,如果无效则移除 testWhileIdle 了解了这些我们对于需要开发的连接池就很轻松了: 实现PooledObjectFactory(JenkinsFactory)该工厂类就是负责JenkinsClient的生命周期 自定义连接池Pool,通过组合的方式引入框架的连接池GenericObjectPool,当然我们也可以用继承的方式来实现(组合优先于继承) 五:反思 连接池写完,目前也只是在测试环境运行,还在观察阶段 有个特别的问题也需要指出来,该问题是笔者在开发时没有注意的问题,也是此次线上产生问题的原因 笔者将原来更新频率从15s调整到了10s,问题就暴露出来了,对于1个job,可能会拉出上百个build,每次会调用3个api接口,如果每次有十个job,每次更新会在10秒内完成,随着job增加,和构建历史增加(虽然有设置保留多少版本,但是api还是会拉出很奇怪的历史build),会超量发出大量http请求。所以我在代码层面也做了改动,每次只更新每个job的前5个最新的build,这样下来,请求量会降低很多 List<Build> buildList = builds.stream().sorted(Comparator.comparing(Build::getNumber).reversed()).limit(5).collect(toList()); by陈朗: 本次事故整体来讲,还是笔者技术有限,解决问题时绕了很多弯,花了大量时间研究源码。我也总结了以下几点 对于连接、锁等这些可能会阻塞的场景,都需要给出超时设置 资源消耗型,需要有池化的思想,提高资源利用率,保证系统稳定 基础很重要,需要持续不断的学习,这样解决问题才能深入底层,找出问题所在,而不是浮于表面 云栖号:https://yqh.aliyun.com第一手的上云资讯,不同行业精选的上云企业案例库,基于众多成功案例萃取而成的最佳实践,助力您上云决策! 原文发布时间:2020-01-18本文作者:槽神本文来自阿里云云栖号合作伙伴“51CTO”,了解相关信息可以关注“51CTO”

优秀的个人博客,低调大师

LC3视点:从阿里云函数计算看Serverless的演进与思考

云计算之所以能够成为DT时代颠覆性力量,是因为其本质是打破传统架构模式,降低成本并简化体系结构,用全新的思维更好的满足了用户需求。而无服务器计算(Serverless Computing)作为这个巨大市场的下一个阶段的进化产物,将真正帮助企业实现只专注于业务和构建应用程序而不必担心IT基础设施,这也将成为云服务商未来竞争的关键。 什么是无服务器计算 云原生计算基金会(Cloud Native Computing Foundation, CNCF)对无服务器计算作了如下定义: Serverless computing refers to the concept of building and running applications that do not require server management. It describ

优秀的个人博客,低调大师

工程师事业的思考(分享一些好的面试题)

题记 最近去参加了一场技术交流会,小圈子内的技术交流,有来自大厂的一些高层工程师,做技术嘛,这条路其实是木有尽头的,说到底还是得要基础好哇。我目前是在做区块链行业,做数字货币交易所,然后很多朋友就是觉得非常不理解了嘛。 就像李笑来说的那样: 可能真的是精英阶层没空关注比特币等数字货币吧。 在去参加交流会之前,虽然说自己工作做Java也有好几年了,但是平时工作很忙,更多的是在做项目,对技术的使用上可能没问题,但是在技术底层,对框架源码、算法和各种协议底层钻研的比较少,现在明确了学习方向,那就以后再接再厉吧。 看了一些关于Java基础方面的面试题的分享,在这里汇总一下,希望对大家能够有所帮助吧。 一.Java面试题全集(上) 二、Java面试题全集(中) 三、Java面试题全集(下) 四、知名互联网公司 Java 开发岗面试知识点解析 希望大家都能在工程师事业上取得理想的成绩。

优秀的个人博客,低调大师

一些比较值得思考和了解的.NET相关的面试题

以前一直是被别人面试,后来“多年的媳妇熬成婆”,走上技术管理岗位之后也面试过别人。对于面试我始终认为有些技巧的,想要在短短半个小时到一两个小时内了解候选人是否满足岗位需要,确实不太容易。 我一般会根据面试者的工作经验状况选择相应的面试形式。对于工作经验较少的面试者(两年及以下)偏向于做题或者实际上机操作为主,因为这样的面试者进来之后也是做一些初级的工作,能够熟练操作软件和了解一些基本常识就够了;如果具有多年工作经验的一般都是通过谈话来了解面试者的水平,俗话说“行家一伸手,便知有没有”,经验丰富的开发人员知道在不同的场合下、不同的阶段要注意些什么问题,通过谈话就能知道对方处在什么水平。 上面说的是本人的经验,下面也谈谈本人几年来在电话面试中被问及的一些有代表性的问题,这些问题多半与.NET有关,如果有兴趣可以看一看。当然在这些我认为值得记录的问题当中绝对没有任何有关控件用法的问题。下面的问题以我回忆的次序为序。 问:请谈一谈ASP.NET1.1和ASP.NET2.0中页面的不同点。 下面请看在ASP.NET1.1中采用页面代码分离模式时ASP.NET页面的声明代码(来自于彬月论坛,我早年曾经以它来学习三层架构): <%@Pagelanguage="c#"Codebehind="ListLWord.aspx.cs"AutoEventWireup="false"Inherits="TraceLWord1.ListLWord"%> 它对应的后台代码声明如下: publicclassListLWord:System.Web.UI.Page 而在ASP.NET2.0中采用页面代码分离模式时的ASP.NET页面代码: <%@PageLanguage="C#"AutoEventWireup="true"CodeFile="Test.aspx.cs"Inherits="Test"%> 它对应的后台代码如下: publicpartialclassTest:System.Web.UI.Page 从代码声明上可以看出ASP.NET1.1中ASP.NET页面HTML代码与cs代码建立关联是通过Codebehind关键字,在后台代码中它是直接继承System.Web.UI.Page的;而在ASP.NET2.0中是通过CodeFile关键字建立关联的,在后台代码中使用了partial关键字,表明当前类是一个局部类。 因为年代比较久远,加之ASP.NET1.1基本没有人在使用,对这两种方式的处理方式的不同我已经记得不太清楚了,记得好像当年有个人专门讲了这种处理方式的优点的。这里要说的是,ASP.NET引擎在背后做的一些工作。Java EE的程序员都会被告知所有的JSP页面在第一次被请求时会被编译成servlet文件,然后再将这些servler文件编译成.class文件文件,以后再次请求的时候就直接运行编译后的.class文件(我当年学JSP的时候别人是这么告诉我的),其实在ASP.NET中也存在着类似的处理流程,并且因为在ASP.NET中处理过程比这个还稍微复杂一点(特别是在使用了代码分离模式时),当第一次请求时编译引擎会将包含HTML代码的aspx页面代码和包含cs代码的.cs文件合成一个.cs文件,然后再将这个.cs文件编译成.dll文件,JIT(即时编译器)会将这些代码编译成本地机器代码,然后运行这些本地机器代码得到的结果返回给客户端,这些本地机器代码会缓存在内存当中,如果在内存没有被清理的情况下再有客户端请求这个页面就会直接运行内存中缓存的本地机器代码,否则就需要再将.dll文件编译成本地机器代码并执行和缓存了。当然,如果对aspx代码或者.cs代码进行了改动,那么下一次请求就会又走一遍整个流程了。 问:请谈一谈二进制序列化和Xml序列化的特点。 二进制序列化和反序列化使用BinaryFormatter类,Xml序列化使用XmlSerializer类。二进制序列化的特点是:无论是否是只读属性,所有成员都将被序列化;序列化的性能高。Xml序列化的特点是:互操作性强;可读性强。 其实关于序列化的类还有SoapFormatter等其它序列化方式,适用于不同的场合。在周公博客上也有关于BinaryFormatter、XmlSerializer和SoapFormatter用法的例子,有兴趣的可以找一找。 问:什么是浅拷贝?什么是深层拷贝?如何实现深层拷贝? 浅拷贝(shallow copy)也叫影子拷贝,在.NET里所有对象的老祖宗Object类里就定义了一个MemberwiseClone()方法来实现浅拷贝。具体做法是创建一个浅表副本,方法是创建一个新对象,然后将当前对象的非静态字段复制到该新对象。 如果字段是值类型的,则对该字段执行逐位复制。如果字段是引用类型,则复制引用但不复制引用的对象;因此,原始对象及其复本引用同一对象。 在浅拷贝里会出现一个问题,那就是如果对对象A实行浅拷贝得到对象B,假设对象A中有引用类型变量M,一旦改变了M的值对象B中对应的变量值也会随之改变,这在有些情况下会产生不可预知的情况,为了解决这个问题,就需要实现深层拷贝(deep copy)。实行深层拷贝之后,不管是A中值类型变量的值被复制到B中,连A中引用类型的字段的值也会被复制到B中,这样改变A中引用类型的值就不会反应到B中对应的字段上去。 如何实现深层拷贝呢?在MSDN中建议我们实现ICloneable来达到实现创建深层副本或者浅表副本的目的,在实际中一般都是采用序列化A,然后将序列化的结果反序列化得到B来实现深层拷贝的目的。 问:请讲一讲ASP.NET命名管道的细节。 ASP.NET命名管道也叫HTTP Pipeline,它是指当客户端的请求经Web服务器到ASP.NET引擎之后的一系列处理过程的细节。关于HTTP流程处理细节在周公拙作《ASP.NET夜话》第十二章中描述过(因为和出版社签订了协议所以没有在博客上发表),如果有想知道细节的请看http://www.cnblogs.com/xiaopin/archive/2010/08/26/1808956.html了解,特别是那些只知道拖拽控件的程序员朋友强烈建议去看一看,看过那个图之后你就会知道原来你随便在网页上拖拽一个控件,这么简单的过程背后却有很多复杂的事情要处理,在其中涉及到了HttpApplication、HttpContext、HttpResponse、HttpRequest、HttpCookie、HttpSession还有HttpSessionState等,除此之外还应该了解一下ASP.NET页面的生命周期,这样你就知道在什么时候可以做什么不可以做什么。 在这里仅仅将作者话的那个图拿出来在这里展示一下,这个图画得相当不错。 问:请问ASP.NET在IIS5、IIS6和IIS7中会有什么不同。 IIS5的ASP.NET请求处理过程 从上图可以看出在IIS中Web服务器和ASP.NET程序分开,Web服务器进程是一个名为inetinfo.exe的非托管程序,而ASP.NET程序进程是一个名为aspnet_wp.exe的进程。 IIS6的ASP.NET请求处理过程 从上图可以看出在IIS6中监听客户端HTTP请求的是http.sys,它在kernel-mode下工作,而处理ASP.NET请求的则是w3wp.exe,它是工作在user-mode下,而在kernel-mode下工作的允许访问所有内存和CPU指令。 IIS7的ASP.NET请求处理过程 在IIS7中处理模式又可以分为两种:经典模式和集成模式。在IIS7中经典模式是兼容IIS6的模式,在IIS6中ASP.NET ISAPI是以插件的形式存在,而在IIS7中则是从插件的角色转换到核心。 IIS6 的执行架构图,以及 IIS7应用程序池配置成经典模式的执行架构图 IIS 7 的执行架构图(集成托管信道模式下的架构) 也就是从IIS5到IIS6的改进主要在http.sys上,而从IIS6到IIS7的改进主要是ISAPI的改进。 注:上面文字和图片来自于对蝈蝈俊.net文章的整理,该篇文章的网址是:http://blog.joycode.com/ghj/archive/2008/07/25/115200.joy 问:请问System.Web.UI.Page类实现了哪个接口?它有定义了哪些方法和属性? 我们知道所有的ASP.NET页面都直接或者间接集成自System.Web.UI.Page类,System.Web.UI.Page类又实现了System.Web.IHttpHandler接口,在这个接口中定义了一个void ProcessRequest(HttpContext context)方法和一个IsReusable属性。IsReusable属性是一个bool值,这个值是指示IHttpHandler的实例是否可再次使用,一般情况下这个值应该设为false,典型的例子就是我们自己编写的Page类,因为它默认设置为false了,所以即使想服务器上同一个页面发送了多次请求,每次得到的都是不同实例。很多初学者经常问:明明我在Page内声明了一个成员变量并赋值了,为什么点击一个按钮PostBack之后就没有值了呢?就是因为不重用实例,提交之后得到的是一个新的实例(建议初学者不要轻易更改这个默认设置来在不同请求间存储变量值)。 关于ASP.NET页面生存周期中的事件和方法如下: 问:如果用.NET语言设计一个类似于Tomcat的Web服务器,你的思路是怎么样的? Tomcat是Java中的一个Java EE容器,支持JSP和servlet。在早年的时候我曾有过模仿Tomcat的想法,所以在当时我也回答了一下我的思路。当时别人问这个问题并不是真的要我用代码去实现这么一个功能,因为大部分人都没有写Web服务器的经历,问这个问题主要是考核我在遇到以前没有遇到过的问题时的反应和思路。 问:数据库死锁是如何产生的?如何解决死锁问题? 如果说上一个问题与.NET还有一点关系的话,那么这个问题其实与.NET没有一点关系了。说实在话在被问及以前我没有遇到过数据库死锁的问题,所以也不知道如何解决。后来查阅了很多资料,对此算是有所了解,如果有人感兴趣,可以参考网址http://www.cnblogs.com/changbluesky/archive/2010/06/10/1753021.html。 总结:在几年的被别人面试的经历中,被问过的问题不光有技术的,也有管理方面的(关于管理方面的我在博客上也谈过几个经典的问题,名字是《技术管理中常见的几个问题》),回忆其中的一些技术方面的问题总结出这些问题来。除了自己总结之外,也是给一些学会了熟练拖拽控件的程序员了解一下在ASP.NET中往深处挖可以学到的知识,当然这些只是点,掌握了这些点之后就可以尝试以点带面了,千万不要停留在表面。 本文转自周金桥51CTO博客,原文链接:http://blog.51cto.com/zhoufoxcn/385287 ,如需转载请自行联系原作

资源下载

更多资源
腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。

用户登录
用户注册