على مدار السنوات العشر الماضية ، كانت حركة المصادر المفتوحة واحدة من المحركات الرئيسية لتطوير صناعة تكنولوجيا المعلومات ، ومكونها الأساسي. أصبح دور المشاريع مفتوحة المصدر أكثر وأكثر أهمية ليس فقط من حيث الكمية ولكن أيضًا من حيث الجودة ، مما يغير مفهوم كيفية وضعها في سوق تكنولوجيا المعلومات بشكل عام. لا يجلس فريق PVS-Studio الشجاع لدينا في وضع الخمول ويقوم بدور نشط في تعزيز وجود البرامج مفتوحة المصدر من خلال العثور على الأخطاء المخفية في أعماق هائلة من الأكواد البرمجية وتقديم خيارات ترخيص مجانية لمؤلفي هذه المشاريع. هذه المقالة ليست سوى جزء آخر من هذا النشاط! اليوم سوف نتحدث عن خلية أباتشي. لدي التقرير - وهناك أشياء تستحق النظر فيها.
حول PVS-Studio
يعد محلل الأكواد الثابتة
PVS-Studio ، الذي ظل موجودًا منذ أكثر من 10 سنوات ، حلاً برمجيًا متعدد الوظائف وسهل الاندماج. في الوقت الحالي ، يدعم C و C ++ و C # و Java ويعمل على Windows و Linux و macOS.
PVS-Studio هو حل B2B مدفوع يستخدمه العديد من الفرق في عدد من الشركات. إذا كنت ترغب في تجربة المحلل ، فقم بزيارة هذه
الصفحة لتنزيل التوزيع وطلب مفتاح تجريبي.
إذا كنت مهووسًا مفتوح المصدر أو ، على سبيل المثال ، طالبًا ، فيمكنك الاستفادة من أحد
خيارات الترخيص المجانية.
حول اباتشي خلية
لقد تزايد حجم البيانات بمعدل هائل على مدار الأعوام الماضية. لم تعد قواعد البيانات القياسية قادرة على مواجهة هذا النمو السريع ، حيث يأتي مصطلح البيانات الضخمة إلى جانب المفاهيم الأخرى ذات الصلة (مثل المعالجة والتخزين والعمليات الأخرى على البيانات الضخمة).
يُعتقد أن
Apache Hadoop هي إحدى تقنيات البيانات الكبيرة الرائدة. وتتمثل مهامها الأساسية في تخزين ومعالجة وإدارة كميات كبيرة من البيانات. المكونات الرئيسية التي تضم الإطار هي Hadoop Common و
HDFS و
Hadoop MapReduce و
Hadoop YARN . بمرور الوقت ، تم تطوير نظام بيئي كبير من المشاريع والتكنولوجيات ذات الصلة حول Hadoop ، والتي بدأ الكثير منها في الأصل كجزء من المشروع ثم تهدأ لتصبح مستقلة.
اباتشي خلية هو واحد منهم.
اباتشي خلية هو مستودع البيانات الموزعة. يدير البيانات المخزنة في HDFS ويوفر لغة الاستعلام على أساس SQL (HiveQL) للتعامل مع هذه البيانات. يمكن الاطلاع على مزيد من التفاصيل حول المشروع
هنا .
تشغيل التحليل
لم يستغرق الكثير من الجهد أو الوقت لبدء التحليل. ها هي الخوارزمية:
- تحميل اباتشي خلية من جيثب ؛
- اقرأ دليل بدء تشغيل أداة تحليل Java وأطلق التحليل ؛
- حصلت على تقرير المحلل ، ودرسته ، وكتب الحالات الأكثر إثارة للاهتمام.
نتائج التحليل هي كما يلي: 1456 تحذيرات من المستويات العالية والمتوسطة (602 و 854 على التوالي) على 6500+ ملف.
ليست كل التحذيرات تشير إلى أخطاء حقيقية. هذا طبيعي جدا يجب عليك تعديل إعدادات المحلل قبل البدء في استخدامه بانتظام. بعد ذلك ، تتوقع عادة معدل منخفض إلى حد ما من الإيجابيات الخاطئة (
مثال ).
لقد تركت تحذيرات 407 (177 عالية و 230 متوسطة المستوى) الناجمة عن ملفات الاختبار. لقد تجاهلت أيضًا تشخيص
V6022 (حيث لا يمكنك التمييز بشكل موثوق بين الأجزاء المعيبة والصحيحة عندما لا تكون على دراية بالكود) ، والذي تم تشغيله حتى 482 مرة. أنا لم أفحص التحذيرات 179 التي تم إنشاؤها بواسطة التشخيص
V6021 .
في النهاية ، لا يزال لدي ما يكفي من التحذيرات ، وبما أنني لم أقوم بتعديل الإعدادات ، فلا تزال هناك بعض النسبة المئوية للإيجابيات الخاطئة فيما بينها. لا يوجد أي نقطة بما في ذلك تحذيرات كثيرة في مقال مثل هذا :). لذلك سنتحدث فقط عن ما لفت انتباهي وبدا فضوليًا بدرجة كافية.
شروط محددة سلفا
من بين التشخيصات التي تم فحصها لهذا التحليل ، يحتفظ
V6007 بسجل لعدد التحذيرات الصادرة. ما يزيد قليلا عن 200 رسالة! البعض يبدو غير ضار ، والبعض الآخر مشبوه ، والبعض الآخر أخطاء حقيقية بعد كل شيء! دعونا نلقي نظرة على بعض منهم.
تعبير
V6007 'key.startsWith ("hplsql.")' صحيح دائمًا. Exec.java (675)
void initOptions() { .... if (key == null || value == null || !key.startsWith("hplsql.")) {
هذا بناء طويلة إذا-آخر-إذا! لا يحب المحلل الحالة في الأخير
إذا (key.startsWith ("hplsql.")) لأنه إذا وصل التنفيذ ، فهذا يعني أنه صحيح. في الواقع ، إذا نظرت إلى السطر الأول من هذا if-if-if ، فسوف ترى أنه يحتوي بالفعل على الاختيار المعاكس ، لذلك إذا لم تبدأ السلسلة بـ
"hplsql". ، سيتخطى التنفيذ على الفور التكرار التالي.
تعبير
V6007 'columnNameProperty.length () == 0' غير صحيح دائمًا. OrcRecordUpdater.java (238)
private static TypeDescription getTypeDescriptionFromTableProperties(....) { .... if (tableProperties != null) { final String columnNameProperty = ....; final String columnTypeProperty = ....; if ( !Strings.isNullOrEmpty(columnNameProperty) && !Strings.isNullOrEmpty(columnTypeProperty)) { List<String> columnNames = columnNameProperty.length() == 0 ? new ArrayList<String>() : ....; List<TypeInfo> columnTypes = columnTypeProperty.length() == 0 ? new ArrayList<TypeInfo>() : ....; .... } } } .... }
المقارنة بين طول سلسلة
العمود اسم المنتج مع الصفر ستعود دائما
خاطئة . يحدث هذا لأن هذه المقارنة تتبع
! Strings.isNullOrEmpty (columnNameProperty) تحقق. لذلك إذا وصل التنفيذ إلى حالتنا ، فسيعني ذلك أن سلسلة
عمودNameProperty ليست فارغة ولا فارغة.
وينطبق الشيء نفسه على سلسلة
عمود columnTypeProperty لاحقًا:
- تعبير V6007 'columnTypeProperty.length () == 0' غير صحيح دائمًا. OrcRecordUpdater.java (239)
تعبير
V6007 'colOrScalar1.equals ("العمود")' غير صحيح دائمًا. GenVectorCode.java (3469)
private void generateDateTimeArithmeticIntervalYearMonth(String[] tdesc) throws Exception { .... String colOrScalar1 = tdesc[4]; .... String colOrScalar2 = tdesc[6]; .... if (colOrScalar1.equals("Col") && colOrScalar1.equals("Column"))
نسخة لصق جيدة القديمة. من وجهة نظر المنطق الحالي ، قد يكون للسلسلة
colOrScalar1 قيمتان مختلفتان في وقت واحد ، وهو أمر مستحيل. من الواضح ، يجب أن تحتوي
الاختبارات على
colOrScalar1 متغير على اليسار و
colOrScalar2 على اليمين.
تحذيرات مماثلة بضعة أسطر أدناه:
- تعبير V6007 'colOrScalar1.equals ("Scalar")' غير صحيح دائمًا. GenVectorCode.java (3475)
- تعبير V6007 'colOrScalar1.equals ("العمود")' غير صحيح دائمًا. GenVectorCode.java (3486)
ونتيجة لذلك ، فإن هذا البناء إن لم يفعل أي شيء آخر.
بعض التحذيرات
V6007 :
- تعبيرات V6007 'character == null' خاطئة دائمًا. RandomTypeUtil.java (43)
- تعبير V6007 'writeIdHwm> 0' غير صحيح دائمًا. TxnHandler.java (1603)
- تعبير V6007 'الحقول. المساواة ("*") "صحيح دائمًا. Server.java (983)
- تعبير V6007 'currentGroups! = Null' صحيح دائمًا. GenericUDFCurrentGroups.java (90)
- تعبير V6007 'this.wh == null' غير صحيح دائمًا. إرجاع جديد مرجع غير فارغ. StorageBasedAuthorizationProvider.java (93) ، StorageBasedAuthorizationProvider.java (92)
- وهلم جرا ...
NPE
V6008 dereference خالية من "dagLock". QueryTracker.java (557) ، QueryTracker.java (553)
private void handleFragmentCompleteExternalQuery(QueryInfo queryInfo) { if (queryInfo.isExternalQuery()) { ReadWriteLock dagLock = getDagLock(queryInfo.getQueryIdentifier()); if (dagLock == null) { LOG.warn("Ignoring fragment completion for unknown query: {}", queryInfo.getQueryIdentifier()); } boolean locked = dagLock.writeLock().tryLock(); ..... } }
يتم اكتشاف كائن فارغ وتسجيله و ... يستمر البرنامج في العمل. نتيجة لذلك ، يتبع التحقق dereference مؤشر فارغة. أوتش!
يجب أن يكون المطورون قد أرادوا بالفعل إنهاء البرنامج من الوظيفة أو التخلص من بعض الاستثناءات الخاصة في حالة الحصول على مرجع خالي.
V6008 dereference من "العازلة" في وظيفة "unlockSingleBuffer". MetadataCache.java (410) ، MetadataCache.java (465)
private boolean lockBuffer(LlapBufferOrBuffers buffers, ....) { LlapAllocatorBuffer buffer = buffers.getSingleLlapBuffer(); if (buffer != null) {
NPE محتمل آخر. إذا وصل التنفيذ إلى أسلوب
unlockSingleBuffer ، فهذا يعني أن الكائن
المخزن المؤقت فارغ. لنفترض أن هذا ما حدث! إذا نظرت إلى طريقة
unlockSingleBuffer ، ستلاحظ كيف يتم
إلغاء تحديد
كائننا في السطر الأول مباشرةً. مسكتك!
تحول ذهب البرية
V6034 التحول من قيمة 'bitShiftsInWord - 1' قد لا يتعارض مع حجم النوع: 'bitShiftsInWord - 1' = [-1 ... 30]. UnsignedInt128.java (1791)
private void shiftRightDestructive(int wordShifts, int bitShiftsInWord, boolean roundUp) { if (wordShifts == 0 && bitShiftsInWord == 0) { return; } assert (wordShifts >= 0); assert (bitShiftsInWord >= 0); assert (bitShiftsInWord < 32); if (wordShifts >= 4) { zeroClear(); return; } final int shiftRestore = 32 - bitShiftsInWord;
هذا تحول محتمل بمقدار -1. إذا تم استدعاء الأسلوب بـ ، على سبيل المثال ،
wordShifts == 3 و
bitShiftsInWord == 0 ، سينتهي السطر المبلغ عنه بـ 1 << -1. هل هذا سلوك مخطط؟
V6034 التحول من قيمة 'j' يمكن أن يكون غير متوافق مع حجم النوع: 'j' = [0 ... 63]. IoTrace.java (272)
public void logSargResult(int stripeIx, boolean[] rgsToRead) { .... for (int i = 0, valOffset = 0; i < elements; ++i, valOffset += 64) { long val = 0; for (int j = 0; j < 64; ++j) { int ix = valOffset + j; if (rgsToRead.length == ix) break; if (!rgsToRead[ix]) continue; val = val | (1 << j);
في السطر الذي تم الإبلاغ عنه ، يمكن أن يكون للمتغير
j قيمة داخل النطاق [0 ... 63]. ولهذا السبب ، قد يتم حساب قيمة
val في الحلقة بطريقة غير متوقعة. في تعبير
(1 << j) ، تكون القيمة 1 من النوع
int ، لذا فإن تحريكها بـ 32 بت وأكثر يأخذنا إلى ما وراء حدود نطاق الكتابة. يمكن إصلاح ذلك عن طريق الكتابة
((طويل) 1 << ي) .
الابتعاد عن طريق قطع الأشجار
V6046 تنسيق غير صحيح. من المتوقع وجود عدد مختلف من عناصر التنسيق. الوسيطات غير المستخدمة: 1 ، 2. StatsSources.java (89)
private static ImmutableList<PersistedRuntimeStats> extractStatsFromPlanMapper (....) { .... if (stat.size() > 1 || sig.size() > 1) { StringBuffer sb = new StringBuffer(); sb.append(String.format( "expected(stat-sig) 1-1, got {}-{} ;",
عند كتابة التعليمات البرمجية لتنسيق السلسلة باستخدام
String.format () ، استخدم المطور بناء جملة خاطئ. نتيجة لذلك ، لم تصل المعلمات التي تم تمريرها إلى السلسلة الناتجة. أعتقد أن المطور كان يعمل على تسجيل الدخول قبل كتابة هذا ، وهو المكان الذي استعار منه بناء الجملة.
استثناء مسروق
V6051 يمكن أن يؤدي استخدام عبارة "المرتجعات" في
المربع "أخيرًا" إلى فقد الاستثناءات غير المعالجة. ObjectStore.java (9080)
private List<MPartitionColumnStatistics> getMPartitionColumnStatistics(....) throws NoSuchObjectException, MetaException { boolean committed = false; try { .... committed = commitTransaction(); return result; } catch (Exception ex) { LOG.error("Error retrieving statistics via jdo", ex); if (ex instanceof MetaException) { throw (MetaException) ex; } throw new MetaException(ex.getMessage()); } finally { if (!committed) { rollbackTransaction(); return Lists.newArrayList(); } } }
يعد إرجاع أي شيء من العنصر
الأخير ممارسة سيئة للغاية ، وهذا المثال يوضح بوضوح السبب.
في كتلة
المحاولة ، يقوم البرنامج بتكوين طلب والوصول إلى وحدة التخزين. المتغير
الملتزم به قيمة
خاطئة بشكل افتراضي ولا يغير حالته إلا بعد تنفيذ جميع الإجراءات السابقة في كتلة
المحاولة بنجاح. هذا يعني أنه إذا تم رفع استثناء ، فسيكون هذا المتغير خطأًا دائمًا.
ستلاحظ كتلة catch الاستثناء وتعديله قليلاً ورميه. لذلك عندما يحين دور المجموعة الأخيرة ، يدخل التنفيذ في الحالة التي سيتم من خلالها إرجاع قائمة فارغة. ماذا كلفت هذه العودة؟ حسنًا ، إنه يكلفنا منع أي استثناء يتم اكتشافه من الإلقاء به إلى الخارج حيث يمكن التعامل معه بشكل صحيح. لن يتم طرح أي من الاستثناءات المحددة في توقيع الطريقة ؛ انهم ببساطة مضللة.
رسالة تشخيص مماثلة:
- V6051 يمكن أن يؤدي استخدام عبارة "المرتجعات" في المربع "أخيرًا" إلى فقد الاستثناءات غير المعالجة. ObjectStore.java (808)
متنوع
دالة
V6009 '
comparTo ' تستقبل وسيطة غريبة. يتم استخدام كائن "o2.getWorkerIdentity ()" كوسيطة لأسلوبه الخاص. LlapFixedRegistryImpl.java (244)
@Override public List<LlapServiceInstance> getAllInstancesOrdered(....) { .... Collections.sort(list, new Comparator<LlapServiceInstance>() { @Override public int compare(LlapServiceInstance o1, LlapServiceInstance o2) { return o2.getWorkerIdentity().compareTo(o2.getWorkerIdentity());
يمكن أن يكون هناك عدد من الأسباب التي تؤدي إلى مثل هذا الخطأ السخيف: معجون النسخ والإهمال والعجل وما إلى ذلك. كثيرا ما نرى أخطاء من هذا القبيل في المشاريع مفتوحة المصدر وحتى لدينا
مقال كامل حول هذا الموضوع.
V6020 قسّم على صفر. يتضمن نطاق قيم المقام المقسوم عليه صفر. SqlMathUtil.java (265)
public static long divideUnsignedLong(long dividend, long divisor) { if (divisor < 0L) { return (compareUnsignedLong(dividend, divisor)) < 0 ? 0L : 1L; } if (dividend >= 0) {
هذا واحد تافهة جدا. سلسلة من الشيكات كانت عاجزة لتفادي القسمة على الصفر.
بعض التحذيرات الأخرى:
- V6020 وزارة الدفاع من الصفر. يتضمن نطاق قيم المقام المقسوم عليه صفر. SqlMathUtil.java (309)
- V6020 قسّم على صفر. يتضمن نطاق قيم المقام المقسوم عليه صفر. SqlMathUtil.java (276)
- V6020 قسّم على صفر. يتضمن نطاق قيم المقام المقسوم عليه صفر. SqlMathUtil.java (312)
V6030 الطريقة الموجودة على يمين "|" سيتم استدعاء المشغل بغض النظر عن قيمة المعامل الأيسر. ربما ، من الأفضل استخدام '||'. OperatorUtils.java (573)
public static Operator<? extends OperatorDesc> findSourceRS(....) { .... List<Operator<? extends OperatorDesc>> parents = ....; if (parents == null | parents.isEmpty()) {
كتب مبرمج المشغل bitwise | بدلا من المنطقية | فهذا يعني أنه سيتم تنفيذ الجزء الأيمن بغض النظر عن نتيجة الجزء الأيسر. إذا كان
الأهل == لاغيين ، سينتهي هذا الخطأ المطبعي بحساب NPE في التعبير الفرعي المنطقي التالي.
V6042 يتم التحقق من التعبير للتأكد من توافقه مع النوع "أ" ولكن يتم تحديده لكتابة "B". VectorColumnAssignFactory.java (347)
public static VectorColumnAssign buildObjectAssign(VectorizedRowBatch outputBatch, int outColIndex, PrimitiveCategory category) throws HiveException { VectorColumnAssign outVCA = null; ColumnVector destCol = outputBatch.cols[outColIndex]; if (destCol == null) { .... } else if (destCol instanceof LongColumnVector) { switch(category) { .... case LONG: outVCA = new VectorLongColumnAssign() { .... } .init(.... , (LongColumnVector) destCol); break; case TIMESTAMP: outVCA = new VectorTimestampColumnAssign() { .... }.init(...., (TimestampColumnVector) destCol);
نحن مهتمون بفصول
LongColumnVector يمتد ColumnVector و
TimestampColumnVector يمتد ColumnVector . التحقق من أن كائن
destCol هو مثيل لـ
LongColumnVector يشير بوضوح إلى أنه كائن من هذه الفئة سيتم التعامل معه في نص البيان الشرطي. على الرغم من ذلك ، ومع ذلك ، فإنه لا يزال يلقي إلى
TimestampColumnVector ! كما ترون ، هذه فئات مختلفة فيما عدا أنها مشتقة من نفس الوالد. نتيجة لذلك ، حصلنا على
ClassCastException .
نفس الشيء صحيح في حالة الإرسال إلى
IntervalDayTimeColumnVector :
- V6042 يتم التحقق من التعبير للتأكد من توافقه مع النوع "أ" ولكن يتم تحديده لكتابة "B". VectorColumnAssignFactory.java (390)
V6060 تم استخدام المرجع "var" قبل أن يتم التحقق منه ضد قيمة خالية. Var.java (402) ، Var.java (395)
@Override public boolean equals(Object obj) { if (getClass() != obj.getClass()) {
سترى هنا فحصًا غريبًا لكائن
var للإلغاء بعد حدوث dereference بالفعل. في هذا السياق ،
var و
obj هما نفس الكائن (
var = (Var) obj ). يتضمن وجود التحقق
الفارغ أن الكائن الذي تم تمريره قد يكون فارغًا. لذا فإن استدعاء
يساوي (خالية) سيؤدي إلى NPE ، بدلاً من
الكاذبة المتوقعة ، مباشرة في السطر الأول. نعم ، الشيك
موجود ، لكن للأسف ، إنه في المكان الخطأ.
بعض الحالات الأخرى المشابهة ، حيث يتم استخدام كائن قبل الفحص:
- V6060 تم استخدام مرجع "القيمة" قبل أن يتم التحقق منه ضد قيمة خالية. ParquetRecordReaderWrapper.java (168) ، ParquetRecordReaderWrapper.java (166)
- V6060 تم استخدام مرجع "defaultConstraintCols" قبل أن يتم التحقق منه ضد القيمة الخالية. HiveMetaStore.java (2539) ، HiveMetaStore.java (2530)
- V6060 تم استخدام مرجع "projIndxLst" قبل التحقق من أنه لاغٍ. RelOptHiveTable.java (683) ، RelOptHiveTable.java (682)
- V6060 تم استخدام مرجع "oldp" قبل أن يتم التحقق منه ضد قيمة خالية. ObjectStore.java (4343) ، ObjectStore.java (4339)
- وهلم جرا ...
استنتاج
إذا كنت مهتمًا أبدًا بالبيانات الكبيرة إذا كان قليلاً فقط ، فمن الصعب أن تكون غير مدرك لمدى أهمية Apache Hive. هذا مشروع شائع ، وهو مشروع كبير ، ويتألف من أكثر من 6500 ملف مصدر (*. java). قام العديد من المطورين بكتابة ذلك على مدار سنوات عديدة ، مما يعني أن هناك الكثير من الأشياء التي يمكن للمحلل الساكن أن يجدها هناك. إنه يثبت مرة أخرى أن التحليل الثابت مهم ومفيد للغاية عند تطوير المشروعات المتوسطة والكبيرة!
المذكرة. الفحوصات لمرة واحدة مثل تلك التي قمت بها هنا جيدة لعرض قدرات المحلل لكنها سيناريو غير مناسب تمامًا لاستخدامه. تم توضيح هذه الفكرة
هنا وهنا . تحليل ثابت هو أن تستخدم بانتظام!
كشف هذا الفحص لخلايا النحل عن عيوب قليلة وشظايا مريبة. إذا واجه مؤلفو Apache Hive هذه المقالة ، فسيسعدنا مساعدتك في المهمة الشاقة المتمثلة في تحسين المشروع.
لا يمكنك أن تتخيل Apache Hive بدون Apache Hadoop ، لذلك قد يقوم Unicorn من PVS-Studio بزيارة إلى ذلك أيضًا. ولكن هذا كل شيء لهذا اليوم. وفي الوقت نفسه ، أدعوك
لتنزيل المحلل والتحقق من المشاريع الخاصة بك.