Ortalama günlük çizelgeleme hacmi iki milyonu aşan tekrarlanan çizelgeleme sorunları nasıl verimli bir şekilde giderilir? Teknik başlıklar

Yazar | Yu Huijuan

Baş Editör | Guo Rui

Sistem, görev planlaması için Quartz'a geçtiğinden beri, günlük planlama hacmi iki milyondan fazla oldu. Planlama miktarı arttıkça, tekrarlanan iş planlama durumu birdenbire ortaya çıkmaya başlar ve izlenecek bir kural yoktur. İnternette net bir çözüm yoktu, bu yüzden Quartz kaynak kodunda hata ayıklamaya başladık ve sonunda sorunu bulduk.

Kaynak kod analizini okuyacak sabrınız yoksa, doğrudan makalenin sonuna çekebilirsiniz, doğrudan ve basit bir çözüm var. Bu makalede kullanılan Quartz sürümü 2.3.0'dır ve JDBC modu işleri depolamak için kullanılır.

hazır

Öncelikle bu makale kod seviyesinde bir analiz makalesi olduğu için Quartz'ın amacını ve kullanımını önceden anlamanız gerekiyor.İnternette birçok güzel makale var ve bunu önceden kendiniz anlayabilirsiniz.

İkinci olarak, kullanıma ek olarak, Quartz çerçevesinin bazı temel kavramlarını da anlamamız gerekir:

  • Quartz, tetikleme işine ateş diyor. TRIGGERSTATE mevcut tetikleyicinin durumudur, PREVFIRE_TIME son tetikleyicinin zamanıdır, NEXTFIRETIME sonraki tetikleyicinin zamanıdır, tekleme, işin belirli bir zamanda tetikleneceği ancak herhangi bir nedenle tetiklenmediği durumu ifade eder.
  • Quartz çalışırken, iki tür iş parçacığı (ikiden fazla tür) olacaktır, biri işleri zamanlamak için planlama iş parçacığı (tek iş parçacığı) ve diğeri işe özel hizmetleri yürütmek için iş havuzudur.
  • Quartz ile gelen tablolar arasında bu makale 3 tabloyu kapsayacak:
  • tabloyu tetikler. Tetikleyiciler tablosu, bir tetikleyicinin PREVFIRETIME (son tetikleme zamanı), NEXT_FIRETIME (sonraki tetikleme zamanı) ve TRIGGERSTATE (mevcut durum) değerlerini kaydeder. Kapsamlı olmasa da, bu makalede yalnızca bunlar kullanılmıştır.
  • masayı kilitler. Quartz destekler dağıtılmış, yani, aynı anda aynı kaynağı yakalayan birden fazla iş parçacığı olacak ve Quartz bu durumu ele almak için bu tabloya güveniyor, ayrıntılar için aşağıya bakın.
  • fired_triggers tablosu. Tetiklenen tetikleme bilgilerini kaydedin.
  • Tetikleyicinin durumu olan TRIGGER_STATE esas olarak aşağıdaki kategorileri içerir:

Şekil 1 Tetikleme durumu değişim şeması

Tetikleyicinin ilk durumu BEKLİYOR ve BEKLİYOR durumundaki tetikleyici tetiklenmeyi bekliyor. Programlama iş parçacığı, tetikleyiciler tablosunu sürekli olarak tarayacak ve tetiklenecek tetiği NEXTFIRETIME'a göre önceden çekecektir.Tetik, programlama iş parçacığı tarafından çekilirse, durumu KAZANILDI olacaktır. Tetik önceden çekildiğinden ve tetikleyicinin gerçek tetikleme süresine ulaşılmadığından, programlama iş parçacığı gerçek tetik zamanına kadar bekleyecek ve ardından tetikleme durumunu KAZANILDI'dan ÇALIŞTIRIYOR'a değiştirecektir. Tetik artık çalıştırılmıyorsa, durumu TAMAMLANDI olarak değiştirin, aksi takdirde BEKLİYOR ve yeni bir döngü başlatın. Bu döngüdeki herhangi bir bağlantı bir istisna atarsa, tetikleme durumu ERROR olur. Tetiği manuel olarak duraklatırsanız, durum DURAKLATILMIŞ olacaktır.

Sorun gidermeye başlayın

Dağıtılmış durumda veri erişimi

Yukarıda belirtildiği gibi, tetikleyicinin durumu veritabanında saklanır ve Quartz dağıtımı destekler, bu nedenle birden fazla Quartz hizmeti başlatılırsa, aynı tetikleyiciyi yakalamak ve tetiklemek için birden fazla zamanlama iş parçacığı olacaktır. MySQL, select deyimini varsayılan olarak yürütür ve kilitli değildir.Birden fazla zamanlama iş parçacığı aynı anda aynı tetikleyiciyi yakalarsa, tetikleyicinin tekrar tekrar planlanmasına neden olur mu? Hadi Quartz'ın bu sorunu nasıl çözdüğüne bir göz atalım.

Öncelikle JobStoreSupport sınıfının executeInNonManagedTXLock () yöntemine bir göz atalım:

Şekil 2 executeInNonManagedTXLock yönteminin özel uygulaması

Bu yöntemin resmi tanıtımı:

/ ** * Verilen kilidi edindikten sonra verilen geri aramayı yürütün. * JobStore'a bağlı olarak, etrafındaki işlem belki * zaten mevcut olduğu varsayılır (yönetilir). * * @ param lockName Alınacak kilidin adı, örneğin * "TRIGGER_ACCESS". Null ise, o zaman kilit alınmaz, ancak * lockCallback hala bir işlemde yürütülür. * /

Başka bir deyişle, gelen geri arama yöntemi, yürütme sırasında belirtilen kilidi taşır ve işlemi açar.Yorum ayrıca, lockName'in belirtilen kilidin adı olduğundan bahseder.lockName boşsa, geri arama yönteminin çalıştırılması kilitte değildir. Koruma altında, ama hala iş başında.

Bu, bu yöntemi yalnızca işlemi garanti etmek için değil, aynı zamanda geri arama yönteminin iş parçacığı güvenliğini sağlamak için de kullanabileceğimiz anlamına gelir.

Ardından, kilidi kapma işlemi olan executeInNonManagedTXLock (...) içindeki getLock (conn, lockName) yöntemine bir göz atalım. Bu yöntem Semafor arabiriminde tanımlanmıştır.Semafor arabirimi, iş parçacıkları veya kaynakları kilitleyerek kaynakları diğer iş parçacıkları tarafından değiştirilmekten korur.Zamanlama bilgilerimiz veritabanında saklandığından, şimdi DBSemaphore.java'daki getLock yönteminin özel uygulamasına bakın. :

Şekil 3 getLock yönteminin özel uygulaması

Hata ayıklama yoluyla expandedSQL ve expandedInsertSQL değişkenlerine bakıyoruz:

Şekil 4 expandedSQL ve expandedInsertSQL'in özel içeriği

Şekil 4'te görülebileceği gibi, getLock yöntemi, kilitler tablosundaki bir satır kilidi (lockName ile belirlenir) aracılığıyla geri arama yönteminin işlem ve iş parçacığı güvenliğini garanti eder. Kilidi aldıktan sonra, getLock yöntemi lockName öğesini threadlocal'a yazar. Elbette releaseLock olduğunda, lockName threadlocal'dan silinecektir.

Sonuç olarak, executeInNonManagedTXLock () yöntemi, dağıtılmış bir durumda bir seferde yalnızca bir iş parçacığının bu yöntemi çalıştırabilmesini sağlar.

Quartz'ın planlama süreci

Şekil 5 Quartz programlama sırası diyagramı

QuartzSchedulerThread, zamanlama iş parçacığının özel uygulamasıdır Şekil 5, iş parçacığı run () yönteminin ana içeriğidir.Şekil yalnızca normal koşullar altında, yani işlemde hiçbir istisna olmadığında işlemden bahseder. Şekilden de görülebileceği gibi, planlama süreci esas olarak aşağıdaki üç adıma bölünmüştür:

1. Tetiklenecek tetiği çekin:

Programlama iş parçacığı, tetikleme bilgisini, bir seferde tetiklenmek üzere olan belirli bir zaman penceresi içinde belirli bir süre içinde çekecektir. Peki, zaman penceresi ve miktar bilgisi nasıl belirlenir? Önce aşağıdaki parametrelere bakalım:

  • idleWaitTime: Varsayılan değer, org.quartz.scheduler.idleWaitTime yapılandırma özelliği aracılığıyla ayarlanabilen 30s'dir.
  • availThreadCount: Her zaman 1'den büyük olan kullanılabilir (boşta) çalışan iş parçacığı sayısını alın, çünkü bu yöntem bir çalışan iş parçacığı serbest kalıncaya kadar engellenecektir.
  • maxBatchSize: Bir seferde çekilebilecek maksimum tetik sayısı, varsayılan değer 1'dir ve org.quartz.scheduler.batchTriggerAcquisitionMaxCount tarafından geçersiz kılınabilir.
  • batchTimeWindow: Zaman penceresi ayarlama parametresi, varsayılan değer 0'dır ve org.quartz.scheduler.batchTriggerAcquisitionFireAheadTimeWindow aracılığıyla yeniden yazılabilir.
  • misfireThreshold: Bu süreden sonra tetiklenmeyen tetikleyiciler, yanlış ateşlenmiş olarak kabul edilir. Varsayılan değer, org.quartz.jobStore.misfireThreshold aracılığıyla ayarlanabilen 60'tır.

Zamanlama iş parçacığı bir kez NEXT_FIRETIME daha az (şimdi + idleWaitTime + batchTimeWindow), daha büyük (now-misfireThreshold), min (availThreadCount, maxBatchSize) tetikleyiciler çeker, varsayılan olarak sonraki 30'ları çeker ve son 60'lar henüz tetiklenmemiştir 1 tetik. Sonra bu tetikleyicilerin durumunu BEKLİYOR'dan KAZANDI olarak değiştirin ve ateşleme tetikleyicileri tablosunu ekleyin.

2. Tetikleyici:

İlk olarak, her tetikleyicinin durumunun ALINMIŞ olup olmadığını kontrol edeceğiz. Eğer öyleyse, durumu ÇALIŞTIRIYOR olarak değiştirin ve ardından tetikleyicinin NEXTFIRETIME değerini güncelleyin. Bu tetikleyicinin NEXTFIRETIME boşsa, yani gelecekte tetiklenmeyecek, ardından durumunu değiştirecektir. KOMPLE için. Tetikleyici eşzamanlı yürütmeye izin vermiyorsa (yani, İşin uygulama sınıfı @ DisallowConcurrentExecution ile işaretlenir), durum BLOCKED olarak değiştirilir, aksi takdirde durum BEKLİYOR olarak değiştirilir.

3. Tetiği sarın ve çalışan iş parçacığı havuzuna atın:

Tetikleyicileri çaprazlayın, eğer tetikleyicilerden birinde ikinci adımda bir hata varsa, yani dönüş değerinde bir istisna veya boş varsa, tetikleyiciler tablosunun ve fired_triggers tablosunun içeriğinde bazı düzeltmeler yapılacak, bu tetikleyiciyi atlayıp bir sonrakini kontrol etmeye devam ediniz. Aksi takdirde, tetik bilgisine dayalı olarak JobRunShell'i (Thread arayüzünü uygulayarak) somutlaştırırız ve Job'u JOB_CLASS_NAME bazında başlatırız ve ardından JobRunShell örneğini çalışma satırına atarız.

JobRunShell'in run () yönteminde Quartz, job.execute () uygulamasının yürütülmesinden önce ve sonra önceden bağlı dinleyiciye bildirimde bulunur. Job.execute () yürütülürken bir istisna atılırsa, jobExEx yürütme sonucu istisna bilgilerini kaydeder Aksi takdirde, herhangi bir istisna atılmazsa jobExEx null olur. Daha sonra farklı jobExEx'e göre, farklı uygulama talimatları instCode elde edilir.

JobRunShell, son veri tablosu güncelleme işlemini tamamlamak için tetikleme bilgilerini, iş bilgilerini ve yürütme talimatlarını triggeredJobComplete () yöntemine iletir. Örneğin, iş yürütülürken bir istisna atılırsa, tetikleme durumu ERROR olarak değiştirilir, ENGELLENMİŞ ise, BEKLİYOR vb. Olarak değiştirilir ve son olarak çalıştırılan tetik, ateşlenen tetikleyiciler tablosundan silinir. Bunların çalışan iş parçacığı havuzunda zaman uyumsuz olarak yapıldığını unutmayın.

Sorun giderme

Önceki makalede Quartz'ın planlama sürecinde 3 (isteğe bağlı) kilitleme davranışı olduğunu görebiliyoruz, neden isteğe bağlı deniyor? Bu üç adım executeInNonManagedTXLock yöntemi tarafından korunduğundan, executeInNonManagedTXLock yöntemi lockName giriş parametresini boş olarak ayarlayarak kilidi iptal edebilir.

Kodu çevirirken, tetiklenecek tetik çekildiğinde ilk adımı gördük:

java korumalı TriggerFiredBundle triggerFired (Connection conn, OperableTrigger trigger) JobPersistenceException {JobDetail job; Calendar cal = null; // Tetikleyicinin silinmediğinden, duraklatılmadığından veya tamamlanmadığından emin olun ... deneyin {// tetik silindiyse durum olacaktır be STATE_DELETED Dize durumu = getDelegate (). selectTriggerState (conn, trigger.getKey ()); if (! state.equals (STATE_ACQUIRED)) {return null;}} catch (SQLException e) {throw new JobPersistenceException ("Couldn't tetikleme durumunu seçin: "+ e.getMessage (), e);}

Kilitlemeden önce, diğer kilitleme yöntemleri gibi, varsayılan olarak LOCKTRIGGERACCESS yerine lockName hakkında bir karar verin:

java genel Listesi < TriggerFiredResult > triggersFired (nihai Liste < Çalıştırılabilir Tetikleyici > tetikleyiciler) JobPersistenceException {// Varsayılan olarak kilitle executeInNonManagedTXLock (LOCK_TRIGGER_ACCESS, yeni TransactionCallback < Liste < TriggerFiredResult > > () {// atlandı}, yeni TransactionValidator < Liste < TriggerFiredResult > > () {// atlandı});}

Hata ayıklama yoluyla, isAcquireTriggersWithinLock () değerinin yanlış olduğu bulundu, bu da gelen lockName değerinin boş olmasına neden olur. Bu işlemi daha net görmek için koda bir günlük ekledim.

Şekil 6 Zamanlama günlüğü

Şekil 6'dan açıkça görülebileceği gibi, tetiklenecek tetik çekildiğinde, varsayılan durum kilitlenmemektir. Bu varsayılan yapılandırmada bir sorun varsa, sık sık tekrarlanan programlama sorunları olacak mı? Aslında öyle değil çünkü Quartz'ın varsayılan olarak iyimser kilitlemeyi benimsemesi, bu da birden fazla iş parçacığının aynı anda aynı tetiği çekmesine izin veriyor. Planlama sürecinin ikinci adımı yangın tetiklemesi olduğunda Quartz'ın ne yaptığına bir göz atalım. Şu anda kilitlendiğini unutmayın:

java korumalı TriggerFiredBundle triggerFired (Connection conn, OperableTrigger trigger) JobPersistenceException {JobDetail job; Calendar cal = null; // Tetikleyicinin silinmediğinden, duraklatılmadığından veya tamamlanmadığından emin olun ... deneyin {// tetik silindiyse durum olacaktır be STATE_DELETED Dize durumu = getDelegate (). selectTriggerState (conn, trigger.getKey ()); if (! state.equals (STATE_ACQUIRED)) {return null;}} catch (SQLException e) {throw new JobPersistenceException ("Couldn't tetikleme durumunu seçin: "+ e.getMessage (), e);}

Zamanlama iş parçacığı, mevcut tetikleme durumunun ALINMIŞ olduğunu bulursa, yani tetik başka iş parçacıkları tarafından tetiklenirse, boş döndürür. Daha önce de bahsettiğimiz gibi, planlama sürecinin üçüncü adımında, bir tetikleyicinin ikinci adımının dönüş değeri boş bulunursa, üçüncü adım atlanacak ve yangın iptal edilecektir. Normal koşullar altında, iyimser kilitleme, tekrarlanan programlamanın meydana gelmemesini sağlayabilir, ancak ABA sorunları kaçınılmazdır. Tekrarlanan planlama gerçekleştiğinde bu günlüğe bir göz atalım:

Şekil 7 Tekrarlanan programlamanın günlüğü

İlk adımda, yani Quartz, uygun tetikleyicileri çekip durumlarını BEKLİYOR'dan KAZANDI'ya değiştirirken 9 ms'den fazla durakladı ve diğer sunucu 9 ms'lik alandan yararlandı. Dosya BEKLEME tamamlandı - > EDİNİLEN-- > YÖNETİM - > Tüm BEKLEME süreci (yani, tam durum değiştirme döngüsü) için aşağıdaki şekle bakın.

Şekil 8 Tekrarlanan programlamanın nedeninin şematik diyagramı

Bu problem nasıl çözülür? Yapılandırma dosyasına org.quartz.jobStore.acquireTriggersWithinLock = true ekleyin, böylece planlama işleminin ilk adımında, yani tetiklenecek tetikleyiciler çekildiğinde durum kilitlenir, yani birden fazla iş parçacığı aynı anda mevcut olmaz. Aynı tetik çekilirse, tekrarlanan programlama tehlikesi önlenir.

Çözüm

Bu problem nasıl çözülür? Yapılandırma dosyasına org.quartz.jobStore.acquireTriggersWithinLock = true ekleyin, böylece planlama işleminin ilk adımında, yani tetiklenecek tetikleyiciler çekildiğinde durum kilitlenir, yani birden fazla iş parçacığı aynı anda mevcut olmaz. Aynı tetik çekilirse, tekrarlanan programlama tehlikesi önlenir.

Deneyim

Soruşturma süreci tamamen sorunsuz geçmedi. Bazı çukurlar ve bazı teknik olmayan deneyimler vardı:

Öğrenme, sürekli iyileştirilmesi ve gözden geçirilmesi gereken bir beceridir. Şahsen, Quartz'ı öğrenmek için, 2.4MB'lik bir kaynak kodunu ilk çevirdiğimde hiçbir fikrim yoktu ve verimsizdi, bu yüzden hemen yönünü değiştirdim, önce çerçevenin çalışma modunu, ne yaptığını, orada hangi modüllerin olduğunu anladım, Nasıl yapılacağını öğrenmek için ana satırı bulun ve ilgili kaynak koduna bakın. Tekrar tekrar kullandıktan sonra bir sorunla karşılaştım ve daha önce okumadığım kaynak kodunu çevirdim ve gittikçe daha pürüzsüz hale geldi.

Daha önce başka meslektaşların öğrenme yöntemlerini de duymuştum ve bunların benim için tamamen uygun olmadığını hissediyorum. Belki herkesin farklı koşulları ve deneyimleri vardır ve öğrenme yöntemleri biraz farklıdır. Sıradan öğrenmede, kendi öğrenme verimliliğinizi hissetmeniz, önerilere başvurmanız, denemeniz, etkisini hissetmeniz, geliştirmeniz gerekir ve neye uygun olduğunuz konusunda giderek daha net hale gelirsiniz. Zamanlama sürecini kısa sözcüklerle düzeltmeme yardımcı olduğu için ustama minnettarım, böylece kaynak kodunu okumak için bu kadar yorucu olmayacağım.

"Deneyimi" ve "olması gerektiği gibi" sorgulamak için eylemsizlik, gözlerinizi kör eder. Büyük ölçekli kodda alışkanlıklarla karıştırılması kolaydır.İlk başta kilitleme yöntemini gördüğümüzde kilitleme tekniğinin harika olduğunu düşündük.Bu yöntem eşzamanlılık problemini çözmek içindir ve kilitlenmeli. , Kilitlendikten sonra eşzamanlılık sorunu yaşanmaz, veritabanı ile birkaç etkileşimin kilitlenmesi ve aniden bir kez kilitlenmemesi nasıl mümkün olabilir? Tetiklenecek tetikleme yöntemini gördüğümde, bir şeylerin yanlış olduğunu hissettim ve günlüğe tıkladım ve aslında kilidinin açık olduğunu buldum.

Günlükler çok önemlidir. Hata ayıklayabilsek de, programın kayıtlar olmadan ABA sorunu olduğunu bulup ispatlayamıyoruz.

En önemli şey sorundan korkmamaktır.Kuvars gibi büyük ölçekli bir çerçeve için bile, sorunu çözmek için mutlaka 2.4MB kaynak kodunu okumak gerekmez. Zaman olduğu sürece sorun çözülebilir, ancak iyi beceriler bu süreyi kısaltabilir ve becerilerimizi gerçek savaşta geliştirmemiz gerekir.

Yazar tanıtımı: Papadai Ar-Ge mühendisi Yu Huijuan, genellikle acemilerin başlamasına yardımcı olmak için bazı makaleler yazmayı sever, bazı yabancı dilleri tercüme eder, beğenileri görmekten çok mutlu olur ve kod kelimelerinin hızının daha hızlı hale gelmesini umar. yuhuijuan.com benim kişisel blogum, hoş geldiniz.

Feragatname: Bu makale yazar tarafından sunulmuştur ve telif hakkı kendisine aittir.

64 yaşındaki Turing Ödülü sahibi, blockchain tarihindeki büyük sorunların üstesinden geldi! Yoğun paylaşım 12 Eylül'de Şangay'da görüşmek üzere!
önceki
Ateşli tek sıralı kahramanların envanteri, annemin artık tek sıralı sıralarım için endişelenmesine gerek yok!
Sonraki
Spor kıyafetleri giymek aynı değil.BMW 1 Serisi sedan M kit versiyonu 199.99-269.900 yuan'a satılıyor
Eski şehrin korunmasıyla ilgili olarak, Dongcheng Bölgesi belediye başkanı bunları bu yıl yapacağını söyledi.
Cennetin gururlu oğlu, Tanrı'nın Timo! Sizin için karanlık tanrı-Timo'nun kapsamlı bir analizi
BYD Yuan EV kullanma deneyimi nedir? Bir ay arabadan bahsettikten sonra, sahibi gerçek hislerini anlatıyor
Zhou Qi, Scola ile çarpıştı, sol dizini yaraladı ve sahayı terk etti
Sıralama stratejisi: Dünya her zaman haksızdır, bu yüzden onunla güvenle yüzleşmelisiniz!
Kalbinizi ele geçirin. Lynk & Co 022.0TD yüksek enerji versiyonu Mart ayı başlarında piyasaya sürülecek / yeni renkli tekerlekler
Hızlı bir bakış: Zhan 16 "King" bu ay piyasaya sürülecek
Jingdong, Liu Qiangdong'un davaya karıştığını itiraf etti!
Xiling Yinshe, sınırlı sayıda köpek düğmesi hatıra mühürü çıkarmak için China Post ile ortak oldu
Luo Yonghao, mermi metin mesajları, TNT ve sınırsız ekranlarla ilgili tüm soruları yanıtladı.
Yirmi kız kardeş sana söylüyor, ezici kız kardeşler için sahip olunması gereken şey, bir hayalet olduğuna inanıyorum!
To Top